feat: caldav client
This commit is contained in:
170
src-tauri/Cargo.lock
generated
170
src-tauri/Cargo.lock
generated
@ -88,7 +88,13 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"chrono",
|
||||||
|
"diqwest",
|
||||||
|
"icalendar",
|
||||||
"log",
|
"log",
|
||||||
|
"minidom",
|
||||||
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
@ -393,6 +399,15 @@ dependencies = [
|
|||||||
"toml 0.9.7",
|
"toml 0.9.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.39"
|
version = "1.2.39"
|
||||||
@ -449,11 +464,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link 0.2.0",
|
"windows-link 0.2.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz-build",
|
||||||
|
"phf 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono-tz-build"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402"
|
||||||
|
dependencies = [
|
||||||
|
"parse-zoneinfo",
|
||||||
|
"phf_codegen 0.11.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@ -464,6 +502,19 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -678,6 +729,30 @@ dependencies = [
|
|||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest_auth"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3054f4e81d395e50822796c5e99ca522e6ba7be98947d6d4b0e5e61640bdb894"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
"hex",
|
||||||
|
"md-5",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "diqwest"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "753559f3659efa905c873c59efad5ef0f5e94912b3dbd2069331c554daa33d81"
|
||||||
|
dependencies = [
|
||||||
|
"digest_auth",
|
||||||
|
"reqwest",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@ -1512,6 +1587,20 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icalendar"
|
||||||
|
version = "0.17.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f25bc68d1c3113be52919708c870cabe55ba0646b9dade87913fe565aa956a3b"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"chrono-tz",
|
||||||
|
"iso8601",
|
||||||
|
"nom",
|
||||||
|
"nom-language",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ico"
|
name = "ico"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@ -1694,6 +1783,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iso8601"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.15"
|
version = "1.0.15"
|
||||||
@ -1924,6 +2022,16 @@ version = "0.1.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.7.6"
|
version = "2.7.6"
|
||||||
@ -1945,6 +2053,15 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minidom"
|
||||||
|
version = "0.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1d0766d3b25519bd1462d4ca35480e1efabe19727bc4c269aba3b255c0a7a66"
|
||||||
|
dependencies = [
|
||||||
|
"rxml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@ -2029,6 +2146,24 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom-language"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -2381,6 +2516,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-zoneinfo"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@ -3135,6 +3279,26 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rxml"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f288457ad3607c08953ca5604229ebb03878a0b4491d8f545d547281c2b3e0c5"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"rxml_validation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rxml_validation"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "826e80413b9a35e9d33217b3dcac04cf95f6559d15944b93887a08be5496c4a4"
|
||||||
|
dependencies = [
|
||||||
|
"compact_str",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.20"
|
version = "1.0.20"
|
||||||
@ -3529,6 +3693,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache"
|
name = "string_cache"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|||||||
@ -27,3 +27,9 @@ tauri-plugin-keep-screen-on = "0.1.2"
|
|||||||
tauri-plugin-upload = "2"
|
tauri-plugin-upload = "2"
|
||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
tauri-plugin-tts = { git = "https://github.com/cfpwastaken/tauri-plugin-tts.git" }
|
tauri-plugin-tts = { git = "https://github.com/cfpwastaken/tauri-plugin-tts.git" }
|
||||||
|
reqwest = { version = "0.12.23", default-features = false, features = ["rustls-tls"] }
|
||||||
|
diqwest = "3.1.0"
|
||||||
|
minidom = "0.17.0"
|
||||||
|
icalendar = { version = "0.17.5", features = ["chrono-tz"] }
|
||||||
|
anyhow = "1.0.100"
|
||||||
|
chrono = "0.4.42"
|
||||||
|
|||||||
389
src-tauri/src/dav.rs
Normal file
389
src-tauri/src/dav.rs
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use diqwest::WithDigestAuth;
|
||||||
|
use icalendar::{Calendar};
|
||||||
|
use minidom::Element;
|
||||||
|
use reqwest::{
|
||||||
|
header::{CONTENT_TYPE, USER_AGENT}, Client, Method, RequestBuilder, Url
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
static PRINCIPAL_BODY: &str = r#"
|
||||||
|
<d:propfind xmlns:d="DAV:">
|
||||||
|
<d:prop>
|
||||||
|
<d:current-user-principal />
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
static HOMESET_BODY: &str = r#"
|
||||||
|
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<d:prop>
|
||||||
|
<c:calendar-home-set />
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
static CAL_BODY: &str = r#"
|
||||||
|
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" >
|
||||||
|
<d:prop>
|
||||||
|
<d:displayname />
|
||||||
|
<d:resourcetype />
|
||||||
|
<c:supported-calendar-component-set />
|
||||||
|
</d:prop>
|
||||||
|
</d:propfind>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
static EVENT_BODY: &str = r#"
|
||||||
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag />
|
||||||
|
<c:calendar-data />
|
||||||
|
</d:prop>
|
||||||
|
<c:filter>
|
||||||
|
<c:comp-filter name="VCALENDAR">
|
||||||
|
<c:comp-filter name="VEVENT" >
|
||||||
|
<c:time-range start="{start}" end="{end}" />
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:filter>
|
||||||
|
</c:calendar-query>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
pub fn find_elems(root: &Element, tag: String) -> Vec<&Element> {
|
||||||
|
let mut elems: Vec<&Element> = Vec::new();
|
||||||
|
|
||||||
|
for el in root.children() {
|
||||||
|
if el.name() == tag {
|
||||||
|
elems.push(el);
|
||||||
|
} else {
|
||||||
|
let ret = find_elems(el, tag.clone());
|
||||||
|
elems.extend(ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elems
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_elem(root: &Element, tag: String) -> Option<&Element> {
|
||||||
|
if root.name() == tag {
|
||||||
|
return Some(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
for el in root.children() {
|
||||||
|
if el.name() == tag {
|
||||||
|
return Some(el);
|
||||||
|
} else {
|
||||||
|
let ret = find_elem(el, tag.clone());
|
||||||
|
if ret.is_some() {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub enum AuthScheme {
|
||||||
|
Basic,
|
||||||
|
Digest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for AuthScheme {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"basic" => Ok(AuthScheme::Basic),
|
||||||
|
"digest" => Ok(AuthScheme::Digest),
|
||||||
|
_ => Err(format!("Invalid auth scheme: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AuthScheme {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
AuthScheme::Basic => write!(f, "Basic"),
|
||||||
|
AuthScheme::Digest => write!(f, "Digest"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ReqwestAuth {
|
||||||
|
fn send_authed(self, scheme: &AuthScheme, username: &str, password: &str) -> impl std::future::Future<Output = anyhow::Result<reqwest::Response>> + Send;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReqwestAuth for RequestBuilder {
|
||||||
|
fn send_authed(self, scheme: &AuthScheme, username: &str, password: &str) -> impl std::future::Future<Output = anyhow::Result<reqwest::Response>> + Send {
|
||||||
|
async move {
|
||||||
|
let res = match scheme {
|
||||||
|
AuthScheme::Basic => self.basic_auth(username, Some(password)).send().await?,
|
||||||
|
AuthScheme::Digest => self.send_with_digest_auth(username, password).await?,
|
||||||
|
};
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn request(
|
||||||
|
client: &Client,
|
||||||
|
method: Method,
|
||||||
|
url: Url,
|
||||||
|
body: String,
|
||||||
|
depth: u8,
|
||||||
|
credentials: &DAVCredentials,
|
||||||
|
) -> anyhow::Result<Element> {
|
||||||
|
let res = client
|
||||||
|
.request(method, url)
|
||||||
|
.header("Depth", depth.to_string())
|
||||||
|
.header(USER_AGENT, "TrafficCue")
|
||||||
|
.header(CONTENT_TYPE, "application/xml")
|
||||||
|
.body(body)
|
||||||
|
.send_authed(&credentials.scheme, &credentials.username, &credentials.password)
|
||||||
|
.await?;
|
||||||
|
let status = res.status();
|
||||||
|
if !status.is_success() {
|
||||||
|
Err(anyhow::anyhow!("Request failed with status: {}", status))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = res.text().await?;
|
||||||
|
let root: Element = text.parse()?;
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
pub struct DAVCredentials {
|
||||||
|
pub scheme: AuthScheme,
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DAVCredentials {
|
||||||
|
pub async fn find_scheme(url: &Url) -> anyhow::Result<AuthScheme> {
|
||||||
|
let client = Client::new();
|
||||||
|
let res = client
|
||||||
|
.request(Method::from_bytes(b"PROPFIND").expect("Invalid method"), url.clone())
|
||||||
|
.header(USER_AGENT, "TrafficCue")
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
if res.is_err() {
|
||||||
|
return Err(anyhow::anyhow!("Failed to connect to server"));
|
||||||
|
}
|
||||||
|
let res = res.unwrap();
|
||||||
|
if res.status().is_success() {
|
||||||
|
return Err(anyhow::anyhow!("Server did not require authentication"));
|
||||||
|
}
|
||||||
|
let headers = res.headers();
|
||||||
|
if let Some(www_auth) = headers.get("www-authenticate") {
|
||||||
|
let www_auth = www_auth.to_str().unwrap_or("");
|
||||||
|
if www_auth.to_lowercase().contains("digest") {
|
||||||
|
return Ok(AuthScheme::Digest);
|
||||||
|
} else if www_auth.to_lowercase().contains("basic") {
|
||||||
|
return Ok(AuthScheme::Basic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(anyhow::anyhow!("Could not determine authentication scheme"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Client for interacting with a CalDAV server.
|
||||||
|
/// ## Example
|
||||||
|
/// ```
|
||||||
|
/// let client = DAVClient::new("https://cal.example.com/.well-known/caldav", DAVCredentials {
|
||||||
|
/// username: "user".into(),
|
||||||
|
/// password: "pass".into(),
|
||||||
|
/// });
|
||||||
|
/// client.init().await?;
|
||||||
|
/// let calendars = client.get_calendars().await?;
|
||||||
|
/// for cal in calendars {
|
||||||
|
/// println!("{}", cal);
|
||||||
|
/// let events = cal.get_events(&client.credentials).await?;
|
||||||
|
/// for event in events {
|
||||||
|
/// println!("Event:\n{}", event);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct DAVClient {
|
||||||
|
pub url: Url,
|
||||||
|
pub credentials: DAVCredentials,
|
||||||
|
client: Client,
|
||||||
|
|
||||||
|
principal_url: Option<Url>,
|
||||||
|
cal_url: Option<Url>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DAVClient {
|
||||||
|
/// **Ensure the URL is actually the CalDAV endpoint, and not just the base URL.**
|
||||||
|
pub fn new(string_url: &str, credentials: DAVCredentials) -> Self {
|
||||||
|
let url = Url::parse(string_url).expect("Invalid URL");
|
||||||
|
Self {
|
||||||
|
url,
|
||||||
|
credentials,
|
||||||
|
client: Client::new(),
|
||||||
|
principal_url: None,
|
||||||
|
cal_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn init(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.get_principal().await?;
|
||||||
|
self.get_cal_homeset().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn _propfind(
|
||||||
|
&mut self,
|
||||||
|
url: Url,
|
||||||
|
body: String,
|
||||||
|
depth: u8,
|
||||||
|
) -> anyhow::Result<Element> {
|
||||||
|
let method = Method::from_bytes(b"PROPFIND").expect("Invalid method");
|
||||||
|
let root = request(
|
||||||
|
&self.client,
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body,
|
||||||
|
depth,
|
||||||
|
&self.credentials,
|
||||||
|
).await?;
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_principal(&mut self) -> anyhow::Result<()> {
|
||||||
|
let root: Element = self
|
||||||
|
._propfind(self.url.clone(), PRINCIPAL_BODY.to_string(), 0)
|
||||||
|
.await?;
|
||||||
|
let principal = find_elem(&root, "current-user-principal".to_string())
|
||||||
|
.ok_or(anyhow::anyhow!("No principal found"))?;
|
||||||
|
let principal_href =
|
||||||
|
find_elem(principal, "href".to_string()).ok_or(anyhow::anyhow!("No href found"))?;
|
||||||
|
let h_str = principal_href.text();
|
||||||
|
let mut p_url = self.url.clone();
|
||||||
|
p_url.set_path(&h_str);
|
||||||
|
self.principal_url = Some(p_url);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_cal_homeset(&mut self) -> anyhow::Result<()> {
|
||||||
|
if self.principal_url.is_none() {
|
||||||
|
return Err(anyhow::anyhow!("Principal URL not set"));
|
||||||
|
}
|
||||||
|
let root: Element = self
|
||||||
|
._propfind(
|
||||||
|
self.principal_url.clone().unwrap(),
|
||||||
|
HOMESET_BODY.to_string(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let homeset = find_elem(&root, "calendar-home-set".to_string())
|
||||||
|
.ok_or(anyhow::anyhow!("No calendar-home-set found"))?;
|
||||||
|
let href =
|
||||||
|
find_elem(homeset, "href".to_string()).ok_or(anyhow::anyhow!("No href found"))?;
|
||||||
|
let h_str = href.text();
|
||||||
|
let mut cal_url = self.url.clone();
|
||||||
|
cal_url.set_path(&h_str);
|
||||||
|
self.cal_url = Some(cal_url.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_calendars(&mut self) -> anyhow::Result<Vec<DAVCalendar>> {
|
||||||
|
if self.cal_url.is_none() {
|
||||||
|
return Err(anyhow::anyhow!("Calendar URL not set"));
|
||||||
|
}
|
||||||
|
let root: Element = self
|
||||||
|
._propfind(self.cal_url.clone().unwrap(), CAL_BODY.to_string(), 1)
|
||||||
|
.await?;
|
||||||
|
let reps = find_elems(&root, "response".to_string());
|
||||||
|
let mut calendars = Vec::new();
|
||||||
|
for rep in reps {
|
||||||
|
let displayname = find_elem(rep, "displayname".to_string())
|
||||||
|
.ok_or(anyhow::anyhow!("No displayname found"))?
|
||||||
|
.text();
|
||||||
|
if displayname == "" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resourcetype = find_elem(rep, "resourcetype".to_string())
|
||||||
|
.ok_or(anyhow::anyhow!("No resourcetype found"))?;
|
||||||
|
let is_calendar = find_elem(resourcetype, "calendar".to_string()).is_some();
|
||||||
|
if !is_calendar {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url_base = self.cal_url.as_ref().unwrap();
|
||||||
|
let mut href = url_base.clone();
|
||||||
|
href.set_path(
|
||||||
|
&find_elem(rep, "href".to_string())
|
||||||
|
.ok_or(anyhow::anyhow!("No href found"))?
|
||||||
|
.text(),
|
||||||
|
);
|
||||||
|
calendars.push(DAVCalendar {
|
||||||
|
name: displayname,
|
||||||
|
url: href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(calendars)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a CalDAV calendar.
|
||||||
|
/// Can be used to fetch events.
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct DAVCalendar {
|
||||||
|
pub name: String,
|
||||||
|
pub url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DAVCalendar {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} ({})", self.name, self.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DAVCalendar {
|
||||||
|
pub fn new(name: &str, url: Url) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn _report(
|
||||||
|
&mut self,
|
||||||
|
credentials: &DAVCredentials,
|
||||||
|
body: String,
|
||||||
|
) -> anyhow::Result<Element> {
|
||||||
|
let client = Client::new();
|
||||||
|
let method = Method::from_bytes(b"REPORT").expect("Invalid method");
|
||||||
|
let root = request(
|
||||||
|
&client,
|
||||||
|
method,
|
||||||
|
self.url.clone(),
|
||||||
|
body,
|
||||||
|
1,
|
||||||
|
credentials,
|
||||||
|
).await?;
|
||||||
|
Ok(root)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_events(
|
||||||
|
&mut self,
|
||||||
|
credentials: &DAVCredentials,
|
||||||
|
) -> anyhow::Result<Vec<Calendar>> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let time = chrono::Utc::now();
|
||||||
|
let start = time.format("%Y%m%dT000000Z").to_string();
|
||||||
|
let end = time.format("%Y%m%dT235959Z").to_string();
|
||||||
|
let body = EVENT_BODY.replace("{start}", &start).replace("{end}", &end);
|
||||||
|
let root = self._report(credentials, body).await?;
|
||||||
|
let datas = find_elems(&root, "calendar-data".to_string());
|
||||||
|
for data in datas {
|
||||||
|
let etext = data.text();
|
||||||
|
let cal: Calendar = etext.parse().unwrap();
|
||||||
|
events.push(cal);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(events)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src-tauri/src/dav_commands.rs
Normal file
64
src-tauri/src/dav_commands.rs
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
use chrono::Local;
|
||||||
|
use icalendar::{CalendarComponent, Component, DatePerhapsTime, EventLike};
|
||||||
|
|
||||||
|
use crate::dav::{DAVClient, DAVCalendar, DAVCredentials};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn dav_find_scheme(url: &str) -> Result<String, String> {
|
||||||
|
let url = reqwest::Url::parse(url).map_err(|e| format!("Invalid URL: {}", e))?;
|
||||||
|
let scheme = DAVCredentials::find_scheme(&url).await.map_err(|e| format!("Failed to determine authentication scheme: {}", e))?;
|
||||||
|
Ok(scheme.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn dav_fetch_calendars(url: &str, credentials: DAVCredentials) -> Result<Vec<DAVCalendar>, String> {
|
||||||
|
let mut client = DAVClient::new(url, credentials);
|
||||||
|
client.init().await.map_err(|e| format!("Failed to initialize DAV client: {}", e))?;
|
||||||
|
let calendars = client.get_calendars().await.map_err(|e| format!("Failed to fetch calendars: {}", e))?;
|
||||||
|
Ok(calendars)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct DAVEvent {
|
||||||
|
summary: String,
|
||||||
|
start: Option<String>,
|
||||||
|
end: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
location: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dateperhapstime_to_string(d: DatePerhapsTime) -> Option<String> {
|
||||||
|
match d {
|
||||||
|
DatePerhapsTime::DateTime(dt) => dt.try_into_utc(),
|
||||||
|
DatePerhapsTime::Date(d) => d
|
||||||
|
.and_hms_opt(0, 0, 0)
|
||||||
|
.and_then(|dt| dt.and_local_timezone(Local).earliest())
|
||||||
|
.map(|dt| dt.to_utc()),
|
||||||
|
}.map(|dt| dt.to_rfc3339())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn dav_fetch_events(calendar: DAVCalendar, credentials: DAVCredentials) -> Result<Vec<DAVEvent>, String> {
|
||||||
|
let events = calendar.clone().get_events(&credentials).await.map_err(|e| format!("Failed to fetch events: {}", e))?;
|
||||||
|
let mut res: Vec<DAVEvent> = Vec::new();
|
||||||
|
for event in events {
|
||||||
|
for component in &event.components {
|
||||||
|
if let CalendarComponent::Event(event) = component {
|
||||||
|
let location = event.get_location();
|
||||||
|
if location.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start_at = event.get_start().and_then(dateperhapstime_to_string);
|
||||||
|
let end_at = event.get_end().and_then(dateperhapstime_to_string);
|
||||||
|
res.push(DAVEvent {
|
||||||
|
summary: event.get_summary().unwrap_or("No summary").to_string(),
|
||||||
|
start: start_at,
|
||||||
|
end: end_at,
|
||||||
|
description: event.get_description().map(|s| s.to_string()),
|
||||||
|
location: location.map(|s| s.to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
mod dav;
|
||||||
|
mod dav_commands;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@ -15,6 +18,7 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![dav_commands::dav_find_scheme, dav_commands::dav_fetch_calendars, dav_commands::dav_fetch_events])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
|
CalendarSearchIcon,
|
||||||
CloudUploadIcon,
|
CloudUploadIcon,
|
||||||
HandIcon,
|
HandIcon,
|
||||||
MapIcon,
|
MapIcon,
|
||||||
@ -24,6 +25,7 @@
|
|||||||
syncStores,
|
syncStores,
|
||||||
updateStore,
|
updateStore,
|
||||||
} from "$lib/services/stores.svelte";
|
} from "$lib/services/stores.svelte";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
const dev = getDeveloperToggle();
|
const dev = getDeveloperToggle();
|
||||||
|
|
||||||
@ -54,6 +56,45 @@
|
|||||||
await downloadPMTiles(url, name);
|
await downloadPMTiles(url, name);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<SettingsButton
|
||||||
|
icon={CalendarSearchIcon}
|
||||||
|
text="Fetch calendars"
|
||||||
|
onclick={async () => {
|
||||||
|
const url = prompt("URL?");
|
||||||
|
if (!url) return;
|
||||||
|
const scheme = await invoke("dav_find_scheme", { url })
|
||||||
|
.catch((err) => {
|
||||||
|
alert("Error fetching scheme: " + err);
|
||||||
|
});
|
||||||
|
alert("Found scheme: " + scheme);
|
||||||
|
const username = prompt("Username?");
|
||||||
|
const password = prompt("Password?");
|
||||||
|
if (!username || !password) return;
|
||||||
|
const credentials = { scheme, username, password };
|
||||||
|
invoke("dav_fetch_calendars", { url, credentials }).then((calendars) => {
|
||||||
|
alert("Fetched calendars: " + JSON.stringify(calendars));
|
||||||
|
}).catch((err) => {
|
||||||
|
alert("Error fetching calendars: " + err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SettingsButton
|
||||||
|
icon={CalendarSearchIcon}
|
||||||
|
text="Fetch CalDAV events"
|
||||||
|
onclick={async () => {
|
||||||
|
const url = prompt("URL?");
|
||||||
|
const scheme = prompt("Scheme? (basic, digest)");
|
||||||
|
const username = prompt("Username?");
|
||||||
|
const password = prompt("Password?");
|
||||||
|
if (!url || !username || !password) return;
|
||||||
|
const credentials = { scheme, username, password };
|
||||||
|
invoke("dav_fetch_events", { calendar: { name: "Calendar", url }, credentials }).then((events) => {
|
||||||
|
alert("Fetched events: " + JSON.stringify(events));
|
||||||
|
}).catch((err) => {
|
||||||
|
alert("Error fetching events: " + err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
Reference in New Issue
Block a user