mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 13:32:16 +00:00
Compare commits
53 Commits
ea7196501e
...
v0.11.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef40f5ea8c | ||
|
|
1230e29243 | ||
|
|
1b2296c00a | ||
|
|
ac6ab0ca9a | ||
|
|
6312f52b10 | ||
|
|
ec28cb9d9a | ||
|
|
4b4210b4d7 | ||
|
|
8fadff1b57 | ||
|
|
61a8c32af4 | ||
|
|
a45e0b2efd | ||
|
|
eecc03b7b7 | ||
|
|
e8303b9c82 | ||
|
|
a686286d06 | ||
|
|
d81074de3b | ||
|
|
42386adcfa | ||
|
|
d2f5f7c89b | ||
|
|
15e431ce12 | ||
|
|
96a16951f4 | ||
|
|
a32b766c0c | ||
|
|
7a101b7364 | ||
|
|
57275a10b4 | ||
|
|
af239e34bf | ||
|
|
e99b1d9123 | ||
|
|
e39657eb29 | ||
|
|
607db62859 | ||
|
|
eba377b980 | ||
|
|
d5c1ddc590 | ||
|
|
a79e1901b8 | ||
|
|
f29c8fa925 | ||
|
|
54f1ee0788 | ||
|
|
96f221f721 | ||
|
|
ba3b64a9c4 | ||
|
|
22a0337375 | ||
|
|
21902e108a | ||
|
|
08f526fa5b | ||
|
|
ac73f3aaff | ||
|
|
9fdc8434db | ||
|
|
85f3d89235 | ||
|
|
092604694a | ||
|
|
8ef24668ba | ||
|
|
416658d069 | ||
|
|
80eae5db9e | ||
|
|
66f541f1c7 | ||
|
|
873b40ad10 | ||
|
|
5588137f73 | ||
|
|
7bf00da0e5 | ||
|
|
be08275cd3 | ||
|
|
381af1b877 | ||
|
|
425d10cb99 | ||
|
|
5cdbb3b9d3 | ||
|
|
547e477eca | ||
|
|
c19c3492c3 | ||
|
|
5878b93d62 |
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
38
.sqlx/query-053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a.json
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM calendarobjects\n WHERE (principal, cal_id, id) NOT IN (\n SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog\n )\n ;\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cal_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted: bool",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "053c17f3b54ae3e153137926115486eb19a801bd73a74230bcf72a9a7254824a"
|
||||||
|
}
|
||||||
12
.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
generated
Normal file
12
.sqlx/query-4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone_id = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 10
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "4a05eda4e23e8652312548b179a1cc16f43768074ab9e7ab7b7783395384984e"
|
||||||
|
}
|
||||||
74
.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
generated
Normal file
74
.sqlx/query-525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc.json
generated
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayname",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone_id",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted_at",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addr_synctoken",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "push_topic",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "525fc4eab8a0f3eacff7e3c78ce809943f817abf8c8f9ae50073924bccdea2dc"
|
||||||
|
}
|
||||||
12
.sqlx/query-583069cbeba5285c63c2b95e989669d3faed66a75fbfc7cd93e5f64b778f45ab.json
generated
Normal file
12
.sqlx/query-583069cbeba5285c63c2b95e989669d3faed66a75fbfc7cd93e5f64b778f45ab.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "583069cbeba5285c63c2b95e989669d3faed66a75fbfc7cd93e5f64b778f45ab"
|
||||||
|
}
|
||||||
74
.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
generated
Normal file
74
.sqlx/query-66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455.json
generated
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT principal, id, displayname, description, \"order\", color, timezone_id, deleted_at, addr_synctoken, push_topic\n FROM birthday_calendars\n INNER JOIN (\n SELECT principal AS addr_principal,\n id AS addr_id,\n synctoken AS addr_synctoken\n FROM addressbooks\n ) ON (principal, id) = (addr_principal, addr_id)\n WHERE principal = ?\n AND (\n (deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted\n OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted\n )\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayname",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timezone_id",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted_at",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addr_synctoken",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "push_topic",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "66d57f2c99ef37b383a478aff99110e1efbc7ce9332f10da4fa69f7594fb7455"
|
||||||
|
}
|
||||||
12
.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
generated
Normal file
12
.sqlx/query-6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "6c039308ad2ec29570ab492d7a0e85fb79c0a4d3b882b74ff1c2786c12324896"
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 7
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "6d08d3a014743da9b445ab012437ec11f81fd86d3b02fc1df07a036c6b47ace2"
|
|
||||||
}
|
|
||||||
12
.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
generated
Normal file
12
.sqlx/query-72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO birthday_calendars (principal, id, displayname, description, \"order\", color, push_topic)\n VALUES (?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "72c7c67f4952ad669ecd54d96bbcb717815081f74575f0a65987163faf9fe30a"
|
||||||
|
}
|
||||||
12
.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
generated
Normal file
12
.sqlx/query-83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "83f0aaf406785e323ac12019ac24f603c53125a1b2326f324c1e2d7b6c690adc"
|
||||||
|
}
|
||||||
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
38
.sqlx/query-c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d.json
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS \"deleted: bool\"\n FROM addressobjects\n WHERE (principal, addressbook_id, id) NOT IN (\n SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog\n )\n ;\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "addressbook_id",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deleted: bool",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c138b1143ac04af4930266ffae0990e82005911c11a683ad565e92335e085f4d"
|
||||||
|
}
|
||||||
12
.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
generated
Normal file
12
.sqlx/query-cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "cadc4ac16b7ac22b71c91ab36ad9dbb1dec943798d795fcbc811f4c651fea02a"
|
||||||
|
}
|
||||||
438
Cargo.lock
generated
438
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
11
Cargo.toml
11
Cargo.toml
@@ -2,7 +2,7 @@
|
|||||||
members = ["crates/*"]
|
members = ["crates/*"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.10.1"
|
version = "0.11.2"
|
||||||
rust-version = "1.91"
|
rust-version = "1.91"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A CalDAV server"
|
description = "A CalDAV server"
|
||||||
@@ -133,7 +133,7 @@ syn = { version = "2.0", features = ["full"] }
|
|||||||
quote = "1.0"
|
quote = "1.0"
|
||||||
proc-macro2 = "1.0"
|
proc-macro2 = "1.0"
|
||||||
heck = "0.5"
|
heck = "0.5"
|
||||||
darling = "0.21"
|
darling = "0.23"
|
||||||
reqwest = { version = "0.12", features = [
|
reqwest = { version = "0.12", features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"charset",
|
"charset",
|
||||||
@@ -148,6 +148,13 @@ ece = { version = "2.3", default-features = false, features = [
|
|||||||
] }
|
] }
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
async-std = { version = "1.13", features = ["attributes"] }
|
async-std = { version = "1.13", features = ["attributes"] }
|
||||||
|
similar-asserts = "1.7"
|
||||||
|
insta = "1.44"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rstest.workspace = true
|
||||||
|
rustical_store_sqlite = { workspace = true, features = ["test"] }
|
||||||
|
insta.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustical_store.workspace = true
|
rustical_store.workspace = true
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ accepted = [
|
|||||||
"AGPL-3.0",
|
"AGPL-3.0",
|
||||||
"GPL-3.0",
|
"GPL-3.0",
|
||||||
"MPL-2.0",
|
"MPL-2.0",
|
||||||
|
"AGPL-3.0-or-later",
|
||||||
|
"GPL-3.0-or-later",
|
||||||
]
|
]
|
||||||
workarounds = ["ring", "chrono", "rustls"]
|
workarounds = ["ring", "chrono", "rustls"]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ rustical_store_sqlite = { workspace = true, features = ["test"] }
|
|||||||
rstest.workspace = true
|
rstest.workspace = true
|
||||||
async-std.workspace = true
|
async-std.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
insta.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum.workspace = true
|
axum.workspace = true
|
||||||
@@ -45,3 +46,4 @@ tower-http.workspace = true
|
|||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
vtimezones-rs.workspace = true
|
vtimezones-rs.workspace = true
|
||||||
|
similar-asserts.workspace = true
|
||||||
|
|||||||
@@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
|||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &cal_query.href {
|
for href in &cal_query.href {
|
||||||
if let Some(filename) = href.strip_prefix(path) {
|
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
|
||||||
|
&& let Some(filename) = href.strip_prefix(path)
|
||||||
|
{
|
||||||
let filename = filename.trim_start_matches('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||||
match store.get_object(principal, cal_id, object_id, false).await {
|
match store.get_object(principal, cal_id, object_id, false).await {
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_owned());
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub struct CompFilterElement {
|
|||||||
pub(crate) time_range: Option<TimeRangeElement>,
|
pub(crate) time_range: Option<TimeRangeElement>,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||||
|
#[allow(clippy::use_self)]
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||||
|
|
||||||
|
|||||||
@@ -188,9 +188,6 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
|
fn set_prop(&mut self, prop: Self::Prop) -> Result<(), rustical_dav::Error> {
|
||||||
if self.read_only {
|
|
||||||
return Err(rustical_dav::Error::PropReadOnly);
|
|
||||||
}
|
|
||||||
match prop {
|
match prop {
|
||||||
CalendarPropWrapper::Calendar(prop) => match prop {
|
CalendarPropWrapper::Calendar(prop) => match prop {
|
||||||
CalendarProp::CalendarColor(color) => {
|
CalendarProp::CalendarColor(color) => {
|
||||||
@@ -263,9 +260,6 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
|
fn remove_prop(&mut self, prop: &CalendarPropWrapperName) -> Result<(), rustical_dav::Error> {
|
||||||
if self.read_only {
|
|
||||||
return Err(rustical_dav::Error::PropReadOnly);
|
|
||||||
}
|
|
||||||
match prop {
|
match prop {
|
||||||
CalendarPropWrapperName::Calendar(prop) => match prop {
|
CalendarPropWrapperName::Calendar(prop) => match prop {
|
||||||
CalendarPropName::CalendarColor => {
|
CalendarPropName::CalendarColor => {
|
||||||
@@ -317,16 +311,11 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
fn get_user_privileges(&self, user: &Principal) -> Result<UserPrivilegeSet, Self::Error> {
|
||||||
if self.cal.subscription_url.is_some() {
|
if self.cal.subscription_url.is_some() || self.read_only {
|
||||||
return Ok(UserPrivilegeSet::owner_write_properties(
|
return Ok(UserPrivilegeSet::owner_write_properties(
|
||||||
user.is_principal(&self.cal.principal),
|
user.is_principal(&self.cal.principal),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if self.read_only {
|
|
||||||
return Ok(UserPrivilegeSet::owner_read(
|
|
||||||
user.is_principal(&self.cal.principal),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(UserPrivilegeSet::owner_only(
|
Ok(UserPrivilegeSet::owner_only(
|
||||||
user.is_principal(&self.cal.principal),
|
user.is_principal(&self.cal.principal),
|
||||||
|
|||||||
@@ -211,6 +211,9 @@ END:VCALENDAR
|
|||||||
<privilege>
|
<privilege>
|
||||||
<read/>
|
<read/>
|
||||||
</privilege>
|
</privilege>
|
||||||
|
<privilege>
|
||||||
|
<write-properties/>
|
||||||
|
</privilege>
|
||||||
<privilege>
|
<privilege>
|
||||||
<read-acl/>
|
<read-acl/>
|
||||||
</privilege>
|
</privilege>
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ async fn test_propfind() {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.trim()
|
.trim()
|
||||||
.replace("\r\n", "\n");
|
.replace("\r\n", "\n");
|
||||||
println!("{output}");
|
similar_asserts::assert_eq!(expected_output, output);
|
||||||
println!("{}, {} \n\n\n", output.len(), expected_output.len());
|
|
||||||
assert_eq!(output, expected_output);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use rustical_store::auth::PrincipalType;
|
|||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
use strum_macros::VariantArray;
|
use strum_macros::VariantArray;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName, Debug)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||||
pub enum PrincipalProp {
|
pub enum PrincipalProp {
|
||||||
// Scheduling Extensions to CalDAV (RFC 6638)
|
// Scheduling Extensions to CalDAV (RFC 6638)
|
||||||
@@ -34,17 +34,17 @@ pub enum PrincipalProp {
|
|||||||
CalendarHomeSet(CalendarHomeSet),
|
CalendarHomeSet(CalendarHomeSet),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName, Debug)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||||
pub enum PrincipalPropWrapper {
|
pub enum PrincipalPropWrapper {
|
||||||
Principal(PrincipalProp),
|
Principal(PrincipalProp),
|
||||||
Common(CommonPropertiesProp),
|
Common(CommonPropertiesProp),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlSerialize, PartialEq, Eq, Clone, VariantArray)]
|
#[derive(XmlSerialize, PartialEq, Eq, Debug, Clone, VariantArray)]
|
||||||
pub enum ReportMethod {
|
pub enum ReportMethod {
|
||||||
// We don't actually support principal-match
|
// We don't actually support principal-match
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
source: crates/caldav/src/principal/tests.rs
|
||||||
|
expression: response
|
||||||
|
---
|
||||||
|
ResponseElement {
|
||||||
|
href: "/caldav/principal/user/",
|
||||||
|
status: None,
|
||||||
|
propstat: [
|
||||||
|
Normal(
|
||||||
|
PropstatElement {
|
||||||
|
prop: PropTagWrapper(
|
||||||
|
[
|
||||||
|
Principal(
|
||||||
|
CalendarUserType(
|
||||||
|
Individual,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
CalendarUserAddressSet(
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
PrincipalUrl(
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
GroupMembership(
|
||||||
|
GroupMembership(
|
||||||
|
[
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/group/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
GroupMemberSet(
|
||||||
|
GroupMemberSet(
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
AlternateUriSet,
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
SupportedReportSet(
|
||||||
|
SupportedReportSet {
|
||||||
|
supported_report: [
|
||||||
|
ReportWrapper {
|
||||||
|
report: PrincipalMatch,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
CalendarHomeSet(
|
||||||
|
CalendarHomeSet(
|
||||||
|
[
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/group/",
|
||||||
|
},
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/user/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Resourcetype(
|
||||||
|
Resourcetype(
|
||||||
|
[
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(
|
||||||
|
Namespace("DAV:"),
|
||||||
|
),
|
||||||
|
"collection",
|
||||||
|
),
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(
|
||||||
|
Namespace("DAV:"),
|
||||||
|
),
|
||||||
|
"principal",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Displayname(
|
||||||
|
Some(
|
||||||
|
"user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
CurrentUserPrincipal(
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
CurrentUserPrivilegeSet(
|
||||||
|
UserPrivilegeSet {
|
||||||
|
privileges: {
|
||||||
|
All,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Owner(
|
||||||
|
Some(
|
||||||
|
HrefElement {
|
||||||
|
href: "/caldav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
source: crates/caldav/src/principal/tests.rs
|
||||||
|
expression: response.serialize_to_string().unwrap()
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CAL:calendar-user-type>INDIVIDUAL</CAL:calendar-user-type>
|
||||||
|
<CAL:calendar-user-address-set>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</CAL:calendar-user-address-set>
|
||||||
|
<principal-URL>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</principal-URL>
|
||||||
|
<group-membership>
|
||||||
|
<href>/caldav/principal/group/</href>
|
||||||
|
</group-membership>
|
||||||
|
<group-member-set>
|
||||||
|
</group-member-set>
|
||||||
|
<alternate-URI-set/>
|
||||||
|
<supported-report-set>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<principal-match/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
</supported-report-set>
|
||||||
|
<CAL:calendar-home-set>
|
||||||
|
<href>/caldav/principal/group/</href>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</CAL:calendar-home-set>
|
||||||
|
<resourcetype>
|
||||||
|
<collection/>
|
||||||
|
<principal/>
|
||||||
|
</resourcetype>
|
||||||
|
<displayname>user</displayname>
|
||||||
|
<current-user-principal>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</current-user-principal>
|
||||||
|
<current-user-privilege-set>
|
||||||
|
<privilege>
|
||||||
|
<all/>
|
||||||
|
</privilege>
|
||||||
|
</current-user-privilege-set>
|
||||||
|
<owner>
|
||||||
|
<href>/caldav/principal/user/</href>
|
||||||
|
</owner>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: crates/caldav/src/principal/tests.rs
|
||||||
|
expression: propfind
|
||||||
|
---
|
||||||
|
PropfindElement {
|
||||||
|
prop: Allprop,
|
||||||
|
include: None,
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
CalDavPrincipalUri,
|
CalDavPrincipalUri,
|
||||||
principal::{PrincipalResource, PrincipalResourceService},
|
principal::{PrincipalResource, PrincipalResourceService},
|
||||||
@@ -14,6 +12,7 @@ use rustical_store_sqlite::{
|
|||||||
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
|
tests::{get_test_calendar_store, get_test_principal_store, get_test_subscription_store},
|
||||||
};
|
};
|
||||||
use rustical_xml::XmlSerializeRoot;
|
use rustical_xml::XmlSerializeRoot;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -64,6 +63,8 @@ async fn test_propfind() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(propfind);
|
||||||
|
|
||||||
let principal = Principal {
|
let principal = Principal {
|
||||||
id: "user".to_string(),
|
id: "user".to_string(),
|
||||||
displayname: None,
|
displayname: None,
|
||||||
@@ -88,5 +89,6 @@ async fn test_propfind() {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let _output = response.serialize_to_string().unwrap();
|
insta::assert_debug_snapshot!(response);
|
||||||
|
insta::assert_snapshot!(response.serialize_to_string().unwrap());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,3 +35,6 @@ percent-encoding.workspace = true
|
|||||||
ical.workspace = true
|
ical.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
strum_macros.workspace = true
|
strum_macros.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
insta.workspace = true
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
let mut not_found = vec![];
|
let mut not_found = vec![];
|
||||||
|
|
||||||
for href in &addressbook_multiget.href {
|
for href in &addressbook_multiget.href {
|
||||||
if let Some(filename) = href.strip_prefix(path) {
|
if let Ok(href) = percent_encoding::percent_decode_str(href).decode_utf8()
|
||||||
|
&& let Some(filename) = href.strip_prefix(path)
|
||||||
|
{
|
||||||
let filename = filename.trim_start_matches('/');
|
let filename = filename.trim_start_matches('/');
|
||||||
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||||
match store
|
match store
|
||||||
@@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(object) => result.push(object),
|
Ok(object) => result.push(object),
|
||||||
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
|
Err(rustical_store::Error::NotFound) => not_found.push(href.to_string()),
|
||||||
Err(err) => return Err(err.into()),
|
Err(err) => return Err(err.into()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_string());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
not_found.push(href.to_owned());
|
not_found.push(href.to_owned());
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ pub mod prop;
|
|||||||
pub mod resource;
|
pub mod resource;
|
||||||
mod service;
|
mod service;
|
||||||
pub use service::*;
|
pub use service::*;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use rustical_dav_push::DavPushExtensionProp;
|
|||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
use strum_macros::VariantArray;
|
use strum_macros::VariantArray;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName, Debug)]
|
||||||
#[xml(unit_variants_ident = "AddressbookPropName")]
|
#[xml(unit_variants_ident = "AddressbookPropName")]
|
||||||
pub enum AddressbookProp {
|
pub enum AddressbookProp {
|
||||||
// CardDAV (RFC 6352)
|
// CardDAV (RFC 6352)
|
||||||
@@ -20,7 +20,7 @@ pub enum AddressbookProp {
|
|||||||
MaxResourceSize(i64),
|
MaxResourceSize(i64),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName, Debug)]
|
||||||
#[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)]
|
#[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)]
|
||||||
pub enum AddressbookPropWrapper {
|
pub enum AddressbookPropWrapper {
|
||||||
Addressbook(AddressbookProp),
|
Addressbook(AddressbookProp),
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
---
|
||||||
|
source: crates/carddav/src/addressbook/tests.rs
|
||||||
|
expression: response
|
||||||
|
---
|
||||||
|
ResponseElement {
|
||||||
|
href: "/carddav/principal/user/yeet/",
|
||||||
|
status: None,
|
||||||
|
propstat: [
|
||||||
|
Normal(
|
||||||
|
PropstatElement {
|
||||||
|
prop: PropTagWrapper(
|
||||||
|
[
|
||||||
|
Addressbook(
|
||||||
|
AddressbookDescription(
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Addressbook(
|
||||||
|
SupportedAddressData(
|
||||||
|
SupportedAddressData {
|
||||||
|
address_data_type: [
|
||||||
|
AddressDataType {
|
||||||
|
content_type: "text/vcard",
|
||||||
|
version: "3.0",
|
||||||
|
},
|
||||||
|
AddressDataType {
|
||||||
|
content_type: "text/vcard",
|
||||||
|
version: "4.0",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Addressbook(
|
||||||
|
SupportedReportSet(
|
||||||
|
SupportedReportSet {
|
||||||
|
supported_report: [
|
||||||
|
ReportWrapper {
|
||||||
|
report: AddressbookMultiget,
|
||||||
|
},
|
||||||
|
ReportWrapper {
|
||||||
|
report: SyncCollection,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Addressbook(
|
||||||
|
MaxResourceSize(
|
||||||
|
10000000,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SyncToken(
|
||||||
|
SyncToken(
|
||||||
|
"github.com/lennart-k/rustical/ns/0",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SyncToken(
|
||||||
|
Getctag(
|
||||||
|
"github.com/lennart-k/rustical/ns/0",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DavPush(
|
||||||
|
Transports(
|
||||||
|
Transports {
|
||||||
|
transports: [
|
||||||
|
WebPush,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DavPush(
|
||||||
|
Topic(
|
||||||
|
"asdasd",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DavPush(
|
||||||
|
SupportedTriggers(
|
||||||
|
SupportedTriggers(
|
||||||
|
[
|
||||||
|
ContentUpdate(
|
||||||
|
ContentUpdate(
|
||||||
|
One,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PropertyUpdate(
|
||||||
|
PropertyUpdate(
|
||||||
|
One,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Resourcetype(
|
||||||
|
Resourcetype(
|
||||||
|
[
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(
|
||||||
|
Namespace("DAV:"),
|
||||||
|
),
|
||||||
|
"collection",
|
||||||
|
),
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(
|
||||||
|
Namespace("urn:ietf:params:xml:ns:carddav"),
|
||||||
|
),
|
||||||
|
"addressbook",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Displayname(
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
CurrentUserPrincipal(
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
CurrentUserPrivilegeSet(
|
||||||
|
UserPrivilegeSet {
|
||||||
|
privileges: {
|
||||||
|
All,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Owner(
|
||||||
|
Some(
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
source: crates/carddav/src/addressbook/tests.rs
|
||||||
|
expression: response.serialize_to_string().unwrap()
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<href>/carddav/principal/user/yeet/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<CARD:supported-address-data>
|
||||||
|
<CARD:address-data-type content-type="text/vcard" version="3.0"/>
|
||||||
|
<CARD:address-data-type content-type="text/vcard" version="4.0"/>
|
||||||
|
</CARD:supported-address-data>
|
||||||
|
<supported-report-set>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<CARD:addressbook-multiget/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
<supported-report>
|
||||||
|
<report>
|
||||||
|
<sync-collection/>
|
||||||
|
</report>
|
||||||
|
</supported-report>
|
||||||
|
</supported-report-set>
|
||||||
|
<max-resource-size>10000000</max-resource-size>
|
||||||
|
<sync-token>github.com/lennart-k/rustical/ns/0</sync-token>
|
||||||
|
<CS:getctag>github.com/lennart-k/rustical/ns/0</CS:getctag>
|
||||||
|
<PUSH:transports>
|
||||||
|
<PUSH:web-push/>
|
||||||
|
</PUSH:transports>
|
||||||
|
<PUSH:topic>asdasd</PUSH:topic>
|
||||||
|
<PUSH:supported-triggers>
|
||||||
|
<PUSH:content-update>
|
||||||
|
<depth>1</depth>
|
||||||
|
</PUSH:content-update>
|
||||||
|
<PUSH:property-update>
|
||||||
|
<depth>1</depth>
|
||||||
|
</PUSH:property-update>
|
||||||
|
</PUSH:supported-triggers>
|
||||||
|
<resourcetype>
|
||||||
|
<collection/>
|
||||||
|
<CARD:addressbook/>
|
||||||
|
</resourcetype>
|
||||||
|
<current-user-principal>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</current-user-principal>
|
||||||
|
<current-user-privilege-set>
|
||||||
|
<privilege>
|
||||||
|
<all/>
|
||||||
|
</privilege>
|
||||||
|
</current-user-privilege-set>
|
||||||
|
<owner>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</owner>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: crates/carddav/src/addressbook/tests.rs
|
||||||
|
expression: propfind
|
||||||
|
---
|
||||||
|
PropfindElement {
|
||||||
|
prop: Allprop,
|
||||||
|
include: None,
|
||||||
|
}
|
||||||
49
crates/carddav/src/addressbook/tests.rs
Normal file
49
crates/carddav/src/addressbook/tests.rs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
use crate::{CardDavPrincipalUri, addressbook::resource::AddressbookResource};
|
||||||
|
use rustical_dav::resource::Resource;
|
||||||
|
use rustical_store::{Addressbook, auth::Principal};
|
||||||
|
use rustical_xml::XmlSerializeRoot;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_propfind() {
|
||||||
|
let propfind = AddressbookResource::parse_propfind(
|
||||||
|
r#"<?xml version="1.0" encoding="UTF-8"?><propfind xmlns="DAV:"><allprop/></propfind>"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(propfind);
|
||||||
|
|
||||||
|
let principal = Principal {
|
||||||
|
id: "user".to_string(),
|
||||||
|
displayname: None,
|
||||||
|
principal_type: rustical_store::auth::PrincipalType::Individual,
|
||||||
|
password: None,
|
||||||
|
memberships: vec!["group".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let addressbook = Addressbook {
|
||||||
|
id: "yeet".to_string(),
|
||||||
|
principal: "user".to_string(),
|
||||||
|
displayname: None,
|
||||||
|
description: None,
|
||||||
|
deleted_at: None,
|
||||||
|
synctoken: 0,
|
||||||
|
push_topic: "asdasd".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let resource = AddressbookResource(addressbook.clone());
|
||||||
|
let response = resource
|
||||||
|
.propfind(
|
||||||
|
&format!(
|
||||||
|
"/carddav/principal/{}/{}",
|
||||||
|
addressbook.principal, addressbook.id
|
||||||
|
),
|
||||||
|
&propfind.prop,
|
||||||
|
propfind.include.as_ref(),
|
||||||
|
&CardDavPrincipalUri("/carddav"),
|
||||||
|
&principal,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(response);
|
||||||
|
insta::assert_snapshot!(response.serialize_to_string().unwrap());
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ mod service;
|
|||||||
pub use service::*;
|
pub use service::*;
|
||||||
mod prop;
|
mod prop;
|
||||||
pub use prop::*;
|
pub use prop::*;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PrincipalResource {
|
pub struct PrincipalResource {
|
||||||
principal: Principal,
|
pub principal: Principal,
|
||||||
members: Vec<String>,
|
pub members: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for PrincipalResource {
|
impl ResourceName for PrincipalResource {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rustical_dav::{
|
|||||||
};
|
};
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName, Debug)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||||
pub enum PrincipalProp {
|
pub enum PrincipalProp {
|
||||||
// WebDAV Access Control (RFC 3744)
|
// WebDAV Access Control (RFC 3744)
|
||||||
@@ -27,10 +27,10 @@ pub enum PrincipalProp {
|
|||||||
PrincipalAddress(Option<HrefElement>),
|
PrincipalAddress(Option<HrefElement>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName, Debug)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
|
||||||
pub enum PrincipalPropWrapper {
|
pub enum PrincipalPropWrapper {
|
||||||
Principal(PrincipalProp),
|
Principal(PrincipalProp),
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
---
|
||||||
|
source: crates/carddav/src/principal/tests.rs
|
||||||
|
expression: response
|
||||||
|
---
|
||||||
|
ResponseElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
status: None,
|
||||||
|
propstat: [
|
||||||
|
Normal(
|
||||||
|
PropstatElement {
|
||||||
|
prop: PropTagWrapper(
|
||||||
|
[
|
||||||
|
Principal(
|
||||||
|
PrincipalUrl(
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
GroupMembership(
|
||||||
|
GroupMembership(
|
||||||
|
[
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/group/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
GroupMemberSet(
|
||||||
|
GroupMemberSet(
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
AlternateUriSet,
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
PrincipalCollectionSet(
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
AddressbookHomeSet(
|
||||||
|
AddressbookHomeSet(
|
||||||
|
[
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/group/",
|
||||||
|
},
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Principal(
|
||||||
|
PrincipalAddress(
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Resourcetype(
|
||||||
|
Resourcetype(
|
||||||
|
[
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(
|
||||||
|
Namespace("DAV:"),
|
||||||
|
),
|
||||||
|
"collection",
|
||||||
|
),
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(
|
||||||
|
Namespace("DAV:"),
|
||||||
|
),
|
||||||
|
"principal",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Displayname(
|
||||||
|
Some(
|
||||||
|
"user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
CurrentUserPrincipal(
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
CurrentUserPrivilegeSet(
|
||||||
|
UserPrivilegeSet {
|
||||||
|
privileges: {
|
||||||
|
All,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Common(
|
||||||
|
Owner(
|
||||||
|
Some(
|
||||||
|
HrefElement {
|
||||||
|
href: "/carddav/principal/user/",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
source: crates/carddav/src/principal/tests.rs
|
||||||
|
expression: response.serialize_to_string().unwrap()
|
||||||
|
---
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<response xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav" xmlns:CS="http://calendarserver.org/ns/" xmlns:PUSH="https://bitfire.at/webdav-push">
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
<propstat>
|
||||||
|
<prop>
|
||||||
|
<principal-URL>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</principal-URL>
|
||||||
|
<group-membership>
|
||||||
|
<href>/carddav/principal/group/</href>
|
||||||
|
</group-membership>
|
||||||
|
<group-member-set>
|
||||||
|
</group-member-set>
|
||||||
|
<alternate-URI-set/>
|
||||||
|
<principal-collection-set>
|
||||||
|
<href>/carddav/principal/</href>
|
||||||
|
</principal-collection-set>
|
||||||
|
<CARD:addressbook-home-set>
|
||||||
|
<href>/carddav/principal/group/</href>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</CARD:addressbook-home-set>
|
||||||
|
<resourcetype>
|
||||||
|
<collection/>
|
||||||
|
<principal/>
|
||||||
|
</resourcetype>
|
||||||
|
<displayname>user</displayname>
|
||||||
|
<current-user-principal>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</current-user-principal>
|
||||||
|
<current-user-privilege-set>
|
||||||
|
<privilege>
|
||||||
|
<all/>
|
||||||
|
</privilege>
|
||||||
|
</current-user-privilege-set>
|
||||||
|
<owner>
|
||||||
|
<href>/carddav/principal/user/</href>
|
||||||
|
</owner>
|
||||||
|
</prop>
|
||||||
|
<status>HTTP/1.1 200 OK</status>
|
||||||
|
</propstat>
|
||||||
|
</response>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
source: crates/carddav/src/principal/tests.rs
|
||||||
|
expression: propfind
|
||||||
|
---
|
||||||
|
PropfindElement {
|
||||||
|
prop: Allprop,
|
||||||
|
include: None,
|
||||||
|
}
|
||||||
41
crates/carddav/src/principal/tests.rs
Normal file
41
crates/carddav/src/principal/tests.rs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
use rustical_dav::resource::Resource;
|
||||||
|
use rustical_store::auth::Principal;
|
||||||
|
use rustical_xml::XmlSerializeRoot;
|
||||||
|
|
||||||
|
use crate::{CardDavPrincipalUri, principal::PrincipalResource};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_propfind() {
|
||||||
|
let propfind = PrincipalResource::parse_propfind(
|
||||||
|
r#"<?xml version="1.0" encoding="UTF-8"?><propfind xmlns="DAV:"><allprop/></propfind>"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(propfind);
|
||||||
|
|
||||||
|
let principal = Principal {
|
||||||
|
id: "user".to_string(),
|
||||||
|
displayname: None,
|
||||||
|
principal_type: rustical_store::auth::PrincipalType::Individual,
|
||||||
|
password: None,
|
||||||
|
memberships: vec!["group".to_string()],
|
||||||
|
};
|
||||||
|
|
||||||
|
let resource = PrincipalResource {
|
||||||
|
principal: principal.clone(),
|
||||||
|
members: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = resource
|
||||||
|
.propfind(
|
||||||
|
&format!("/carddav/principal/{}", principal.id),
|
||||||
|
&propfind.prop,
|
||||||
|
propfind.include.as_ref(),
|
||||||
|
&CardDavPrincipalUri("/carddav"),
|
||||||
|
&principal,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(response);
|
||||||
|
insta::assert_snapshot!(response.serialize_to_string().unwrap());
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Debug, Clone, PropName, EnumVariants)]
|
||||||
#[xml(unit_variants_ident = "CommonPropertiesPropName")]
|
#[xml(unit_variants_ident = "CommonPropertiesPropName")]
|
||||||
pub enum CommonPropertiesProp {
|
pub enum CommonPropertiesProp {
|
||||||
// WebDAV (RFC 2518)
|
// WebDAV (RFC 2518)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants, Debug)]
|
||||||
#[xml(unit_variants_ident = "SyncTokenExtensionPropName")]
|
#[xml(unit_variants_ident = "SyncTokenExtensionPropName")]
|
||||||
pub enum SyncTokenExtensionProp {
|
pub enum SyncTokenExtensionProp {
|
||||||
// Collection Synchronization (RFC 6578)
|
// Collection Synchronization (RFC 6578)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ pub async fn route_proppatch<R: ResourceService>(
|
|||||||
.get_resource(path_components, false)
|
.get_resource(path_components, false)
|
||||||
.await?;
|
.await?;
|
||||||
let privileges = resource.get_user_privileges(principal)?;
|
let privileges = resource.get_user_privileges(principal)?;
|
||||||
if !privileges.has(&UserPrivilege::Write) {
|
if !privileges.has(&UserPrivilege::WriteProperties) {
|
||||||
return Err(Error::Unauthorized.into());
|
return Err(Error::Unauthorized.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::xml::HrefElement;
|
use crate::xml::HrefElement;
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct GroupMembership(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
pub struct GroupMembership(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct GroupMemberSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
pub struct GroupMemberSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ use headers::{CacheControl, ContentType, HeaderMapExt};
|
|||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use quick_xml::name::Namespace;
|
use quick_xml::name::Namespace;
|
||||||
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
use rustical_xml::{XmlRootTag, XmlSerialize, XmlSerializeRoot};
|
||||||
use std::collections::HashMap;
|
use std::{collections::HashMap, fmt::Debug};
|
||||||
|
|
||||||
#[derive(XmlSerialize)]
|
#[derive(XmlSerialize, Debug)]
|
||||||
pub struct PropTagWrapper<T: XmlSerialize>(#[xml(flatten, ty = "untagged")] pub Vec<T>);
|
pub struct PropTagWrapper<T: XmlSerialize>(#[xml(flatten, ty = "untagged")] pub Vec<T>);
|
||||||
|
|
||||||
// RFC 2518
|
// RFC 2518
|
||||||
@@ -30,7 +30,7 @@ fn xml_serialize_status(
|
|||||||
XmlSerialize::serialize(&format!("HTTP/1.1 {status}"), ns, tag, namespaces, writer)
|
XmlSerialize::serialize(&format!("HTTP/1.1 {status}"), ns, tag, namespaces, writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlSerialize)]
|
#[derive(XmlSerialize, Debug)]
|
||||||
#[xml(untagged)]
|
#[xml(untagged)]
|
||||||
pub enum PropstatWrapper<T: XmlSerialize> {
|
pub enum PropstatWrapper<T: XmlSerialize> {
|
||||||
Normal(PropstatElement<PropTagWrapper<T>>),
|
Normal(PropstatElement<PropTagWrapper<T>>),
|
||||||
@@ -40,7 +40,7 @@ pub enum PropstatWrapper<T: XmlSerialize> {
|
|||||||
// RFC 2518
|
// RFC 2518
|
||||||
// <!ELEMENT response (href, ((href*, status)|(propstat+)),
|
// <!ELEMENT response (href, ((href*, status)|(propstat+)),
|
||||||
// responsedescription?) >
|
// responsedescription?) >
|
||||||
#[derive(XmlSerialize, XmlRootTag)]
|
#[derive(XmlSerialize, XmlRootTag, Debug)]
|
||||||
#[xml(ns = "crate::namespace::NS_DAV", root = "response")]
|
#[xml(ns = "crate::namespace::NS_DAV", root = "response")]
|
||||||
#[xml(ns_prefix(
|
#[xml(ns_prefix(
|
||||||
crate::namespace::NS_DAV = "",
|
crate::namespace::NS_DAV = "",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::{ContentUpdate, PropertyUpdate, SupportedTriggers, Transports, Trigge
|
|||||||
use rustical_dav::header::Depth;
|
use rustical_dav::header::Depth;
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants, Debug)]
|
||||||
#[xml(unit_variants_ident = "DavPushExtensionPropName")]
|
#[xml(unit_variants_ident = "DavPushExtensionPropName")]
|
||||||
pub enum DavPushExtensionProp {
|
pub enum DavPushExtensionProp {
|
||||||
// WebDav Push
|
// WebDav Push
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ impl Default for Transports {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Eq, Clone)]
|
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct SupportedTriggers(#[xml(flatten, ty = "untagged")] pub Vec<Trigger>);
|
pub struct SupportedTriggers(#[xml(flatten, ty = "untagged")] pub Vec<Trigger>);
|
||||||
|
|
||||||
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Eq, Debug, Clone)]
|
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Eq, Debug, Clone)]
|
||||||
|
|||||||
@@ -36,27 +36,27 @@ export class CreateAddressbookForm extends LitElement {
|
|||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group addressbooks)
|
principal (for group addressbooks)
|
||||||
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
<select .value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option .value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map(membership => html`
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
<option value=${membership}>${membership}</option>
|
<option .value=${membership}>${membership}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
|
<input type="text" .value=${this.addr_id} @change=${e => this.addr_id = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
<input type="text" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${e => this.description = e.target.value} />
|
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
|
import { escapeXml } from ".";
|
||||||
|
import { getTimezones } from "./timezones.ts";
|
||||||
|
|
||||||
|
@customElement("create-birthday-calendar-form")
|
||||||
|
export class CreateCalendarForm extends LitElement {
|
||||||
|
protected override createRenderRoot() {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
principal: string = ''
|
||||||
|
@property()
|
||||||
|
addr_id: string = ''
|
||||||
|
@property()
|
||||||
|
displayname: string = ''
|
||||||
|
@property()
|
||||||
|
description: string = ''
|
||||||
|
@property()
|
||||||
|
color: string = ''
|
||||||
|
|
||||||
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
|
@property()
|
||||||
|
timezones: Array<String> = []
|
||||||
|
|
||||||
|
override render() {
|
||||||
|
return html`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
|
||||||
|
<dialog ${ref(this.dialog)}>
|
||||||
|
<h3>Create calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" .value=${this.displayname} required @change=${e => this.displayname = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Color
|
||||||
|
<input type="color" .value=${this.color} @change=${e => this.color = e.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit(e: SubmitEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!this.addr_id) {
|
||||||
|
alert("Empty id")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
|
||||||
|
method: 'MKCOL',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml'
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ''}
|
||||||
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ''}
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
<CAL:comp name="VEVENT" />
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
</mkcol>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
window.location.reload()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'create-calendar-form': CreateCalendarForm
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,30 @@
|
|||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
import { escapeXml } from ".";
|
import { escapeXml, SVG_ICON_CALENDAR, SVG_ICON_INTERNET } from ".";
|
||||||
|
import { getTimezones } from "./timezones.ts";
|
||||||
|
|
||||||
@customElement("create-calendar-form")
|
@customElement("create-calendar-form")
|
||||||
export class CreateCalendarForm extends LitElement {
|
export class CreateCalendarForm extends LitElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
|
this.resetForm()
|
||||||
this.fetchTimezones()
|
this.fetchTimezones()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetForm() {
|
||||||
|
this.form.value?.reset()
|
||||||
|
this.principal = this.user
|
||||||
|
this.cal_id = self.crypto.randomUUID()
|
||||||
|
this.displayname = ''
|
||||||
|
this.description = ''
|
||||||
|
this.timezone_id = ''
|
||||||
|
this.color = ''
|
||||||
|
this.isSubscription = false
|
||||||
|
this.subscriptionUrl = null
|
||||||
|
this.components = new Set(["VEVENT", "VTODO"])
|
||||||
|
}
|
||||||
|
|
||||||
async fetchTimezones() {
|
async fetchTimezones() {
|
||||||
this.timezones = await getTimezones()
|
this.timezones = await getTimezones()
|
||||||
}
|
}
|
||||||
@@ -21,23 +36,23 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
@property()
|
@property()
|
||||||
user: string = ''
|
user: string = ''
|
||||||
@property()
|
@property()
|
||||||
principal: string = ''
|
principal: string
|
||||||
@property()
|
@property()
|
||||||
cal_id: string = self.crypto.randomUUID()
|
cal_id: string
|
||||||
@property()
|
@property()
|
||||||
displayname: string = ''
|
displayname: string
|
||||||
@property()
|
@property()
|
||||||
description: string = ''
|
description: string
|
||||||
@property()
|
@property()
|
||||||
timezone_id: string = ''
|
timezone_id: string
|
||||||
@property()
|
@property()
|
||||||
color: string = ''
|
color: string
|
||||||
@property()
|
@property()
|
||||||
isSubscription: boolean = false
|
isSubscription: boolean
|
||||||
@property()
|
@property()
|
||||||
subscriptionUrl: string = ''
|
subscriptionUrl: string
|
||||||
@property()
|
@property()
|
||||||
components: Set<"VEVENT" | "VTODO" | "VJOURNAL"> = new Set()
|
components: Set<"VEVENT" | "VTODO" | "VJOURNAL">
|
||||||
|
|
||||||
dialog: Ref<HTMLDialogElement> = createRef()
|
dialog: Ref<HTMLDialogElement> = createRef()
|
||||||
form: Ref<HTMLFormElement> = createRef()
|
form: Ref<HTMLFormElement> = createRef()
|
||||||
@@ -46,13 +61,13 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
return html`
|
return html`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
<button @click=${e => this.dialog.value.showModal()}>Create calendar</button>
|
||||||
<dialog ${ref(this.dialog)}>
|
<dialog ${ref(this.dialog)} @close=${e => this.resetForm()}>
|
||||||
<h3>Create calendar</h3>
|
<h3>Create calendar</h3>
|
||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group calendars)
|
principal (for group calendars)
|
||||||
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
<select required value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map(membership => html`
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
<option value=${membership}>${membership}</option>
|
<option value=${membership}>${membership}</option>
|
||||||
@@ -62,17 +77,17 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
|
<input type="text" required .value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
<input type="text" required .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
<select .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||||
<option value="">No timezone</option>
|
<option value="">No timezone</option>
|
||||||
${this.timezones.map(timezone => html`
|
${this.timezones.map(timezone => html`
|
||||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
@@ -82,45 +97,57 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${e => this.description = e.target.value} />
|
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Color
|
Color
|
||||||
<input type="color" name="color" @change=${e => this.color = e.target.value} />
|
<input type="color" .value=${this.color} @change=${e => this.color = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>Type</label>
|
||||||
Calendar is subscription to external calendar
|
<div class="tab-radio">
|
||||||
<input type="checkbox" name="is_subscription" @change=${e => this.isSubscription = e.target.checked} />
|
<label>
|
||||||
</label>
|
<input type="radio" name="type" .checked=${!this.isSubscription} @change=${e => this.isSubscription = false}></input>
|
||||||
|
${SVG_ICON_CALENDAR}
|
||||||
|
Calendar
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="type" .checked=${this.isSubscription} @change=${e => this.isSubscription = true}></input>
|
||||||
|
${SVG_ICON_INTERNET}
|
||||||
|
webCal Subscription
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
${this.isSubscription ? html`
|
${this.isSubscription ? html`
|
||||||
<label>
|
<label>
|
||||||
Subscription URL
|
Subscription URL
|
||||||
<input type="text" name="subscription_url" @change=${e => this.subscriptionUrl = e.target.value} />
|
<input type="text" pattern="https://.*" .required=${this.isSubscription} .value=${this.subscriptionUrl} @change=${e => this.subscriptionUrl = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
|
<br>
|
||||||
`: html``}
|
`: html``}
|
||||||
<br>
|
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
<label>Components</label>
|
||||||
<label>
|
<div>
|
||||||
Support ${comp}
|
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
||||||
<input type="checkbox" value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
<label>
|
||||||
</label>
|
Support ${comp}
|
||||||
<br>
|
<input type="checkbox" .value=${comp} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} .checked=${this.components.has(comp)} />
|
||||||
`)}
|
</label>
|
||||||
|
<br>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close();}} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit(e: SubmitEvent) {
|
async submit(e: SubmitEvent) {
|
||||||
console.log(this.displayname)
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!this.cal_id) {
|
if (!this.cal_id) {
|
||||||
alert("Empty id")
|
alert("Empty id")
|
||||||
@@ -134,6 +161,10 @@ export class CreateCalendarForm extends LitElement {
|
|||||||
alert("No calendar components selected")
|
alert("No calendar components selected")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.isSubscription && !this.subscriptionUrl) {
|
||||||
|
alert("Invalid subscription url")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
|
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
|
||||||
method: 'MKCOL',
|
method: 'MKCOL',
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export class EditAddressbookForm extends LitElement {
|
|||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
<input type="text" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
|
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ export class EditCalendarForm extends LitElement {
|
|||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
<input type="text" required .value=${this.displayname} @change=${e => this.displayname = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
<select .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||||
<option value="">No timezone</option>
|
<option value="">No timezone</option>
|
||||||
${this.timezones.map(timezone => html`
|
${this.timezones.map(timezone => html`
|
||||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
@@ -68,18 +68,18 @@ export class EditCalendarForm extends LitElement {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" .value=${this.description} @change=${e => this.description = e.target.value} />
|
<input type="text" .value=${this.description} @change=${e => this.description = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Color
|
Color
|
||||||
<input type="color" name="color" .value=${this.color} @change=${e => this.color = e.target.value} />
|
<input type="color" .value=${this.color} @change=${e => this.color = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
${["VEVENT", "VTODO", "VJOURNAL"].map(comp => html`
|
||||||
<label>
|
<label>
|
||||||
Support ${comp}
|
Support ${comp}
|
||||||
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
<input type="checkbox" .value=${comp} ?checked=${this.components.has(comp)} @change=${e => e.target.checked ? this.components.add(e.target.value) : this.components.delete(e.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
`)}
|
`)}
|
||||||
|
|||||||
@@ -32,23 +32,25 @@ export class ImportAddressbookForm extends LitElement {
|
|||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group addressbook)
|
principal (for group addressbook)
|
||||||
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
<select name="principal" required .value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option .value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map(membership => html`
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
<option value=${membership}>${membership}</option>
|
<option .value=${membership}>${membership}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.addressbook_id} @change=${e => this.addressbook_id = e.target.value} />
|
<input type="text" required .value=${this.addressbook_id} @change=${e => this.addressbook_id = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
file
|
file
|
||||||
<input type="file" accept="text/vcard" name="file" @change=${e => this.file = e.target.files[0]} />
|
<input type="file" accept="text/vcard" required @change=${e => this.file = e.target.files[0]} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
<button type="submit">Import</button>
|
<button type="submit">Import</button>
|
||||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -32,23 +32,25 @@ export class ImportCalendarForm extends LitElement {
|
|||||||
<form @submit=${this.submit} ${ref(this.form)}>
|
<form @submit=${this.submit} ${ref(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group calendars)
|
principal (for group calendars)
|
||||||
<select name="principal" value=${this.user} @change=${e => this.principal = e.target.value}>
|
<select name="principal" required .value=${this.user} @change=${e => this.principal = e.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option .value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map(membership => html`
|
${window.rusticalUser.memberships.map(membership => html`
|
||||||
<option value=${membership}>${membership}</option>
|
<option .value=${membership}>${membership}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
|
<input type="text" required .value=${this.cal_id} @change=${e => this.cal_id = e.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
file
|
file
|
||||||
<input type="file" accept="text/calendar" name="file" @change=${e => this.file = e.target.files[0]} />
|
<input type="file" required accept="text/calendar" @change=${e => this.file = e.target.files[0]} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
<button type="submit">Import</button>
|
<button type="submit">Import</button>
|
||||||
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
<button type="submit" @click=${event => { event.preventDefault(); this.dialog.value.close(); this.form.value.reset() }} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import {svg} from 'lit'
|
||||||
|
|
||||||
export function escapeXml(unsafe: string): string {
|
export function escapeXml(unsafe: string): string {
|
||||||
return unsafe.replace(/&/g, '&')
|
return unsafe.replace(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replace(/</g, '<')
|
||||||
@@ -5,3 +7,23 @@ export function escapeXml(unsafe: string): string {
|
|||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''')
|
.replace(/'/g, ''')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SVG_ICON_CALENDAR = svg`<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||||
|
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M7 2V6" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M21 10V6C21 4.89543 20.1046 4 19 4H18.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SVG_ICON_INTERNET = svg`<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<svg class="icon" width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M13 2.04932C13 2.04932 16 5.99994 16 11.9999" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M11 21.9506C11 21.9506 8 17.9999 8 11.9999C8 5.99994 11 2.04932 11 2.04932" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.62964 15.5H12" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.62964 8.5H21.3704" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.8789 17.9174C22.3727 18.2211 22.3423 18.9604 21.8337 19.0181L19.2671 19.309L18.1159 21.6213C17.8878 22.0795 17.1827 21.8552 17.0661 21.2873L15.8108 15.1713C15.7123 14.6913 16.1437 14.3892 16.561 14.646L21.8789 17.9174Z"></path>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: [
|
input: [
|
||||||
|
"lib/create-birthday-calendar-form.ts",
|
||||||
"lib/create-calendar-form.ts",
|
"lib/create-calendar-form.ts",
|
||||||
"lib/edit-calendar-form.ts",
|
"lib/edit-calendar-form.ts",
|
||||||
"lib/import-calendar-form.ts",
|
"lib/import-calendar-form.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -34,27 +34,27 @@ let CreateAddressbookForm = class extends i {
|
|||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group addressbooks)
|
principal (for group addressbooks)
|
||||||
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
<select .value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option .value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map((membership) => x`
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
<option value=${membership}>${membership}</option>
|
<option .value=${membership}>${membership}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} />
|
<input type="text" .value=${this.addr_id} @change=${(e2) => this.addr_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
<input type="text" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
|
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
||||||
|
for (var i2 = decorators.length - 1, decorator; i2 >= 0; i2--)
|
||||||
|
if (decorator = decorators[i2])
|
||||||
|
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
||||||
|
if (kind && result) __defProp(target, key, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
let CreateCalendarForm = class extends i {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.principal = "";
|
||||||
|
this.addr_id = "";
|
||||||
|
this.displayname = "";
|
||||||
|
this.description = "";
|
||||||
|
this.color = "";
|
||||||
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
this.timezones = [];
|
||||||
|
}
|
||||||
|
createRenderRoot() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
render() {
|
||||||
|
return x`
|
||||||
|
<button @click=${() => this.dialog.value.showModal()}>Create birthday calendar</button>
|
||||||
|
<dialog ${n(this.dialog)}>
|
||||||
|
<h3>Create calendar</h3>
|
||||||
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
|
<label>
|
||||||
|
Displayname
|
||||||
|
<input type="text" .value=${this.displayname} required @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Description
|
||||||
|
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
Color
|
||||||
|
<input type="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
<button type="submit" @click=${(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.dialog.value.close();
|
||||||
|
this.form.value.reset();
|
||||||
|
}} class="cancel">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
async submit(e2) {
|
||||||
|
e2.preventDefault();
|
||||||
|
if (!this.addr_id) {
|
||||||
|
alert("Empty id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.displayname) {
|
||||||
|
alert("Empty displayname");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let response = await fetch(`/caldav/principal/${this.principal}/_birthdays_${this.addr_id}`, {
|
||||||
|
method: "MKCOL",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/xml"
|
||||||
|
},
|
||||||
|
body: `
|
||||||
|
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CS="http://calendarserver.org/ns/" xmlns:ICAL="http://apple.com/ns/ical/">
|
||||||
|
<set>
|
||||||
|
<prop>
|
||||||
|
<displayname>${escapeXml(this.displayname)}</displayname>
|
||||||
|
${this.description ? `<CAL:calendar-description>${escapeXml(this.description)}</CAL:calendar-description>` : ""}
|
||||||
|
${this.color ? `<ICAL:calendar-color>${escapeXml(this.color)}</ICAL:calendar-color>` : ""}
|
||||||
|
<CAL:supported-calendar-component-set>
|
||||||
|
<CAL:comp name="VEVENT" />
|
||||||
|
</CAL:supported-calendar-component-set>
|
||||||
|
</prop>
|
||||||
|
</set>
|
||||||
|
</mkcol>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
if (response.status >= 400) {
|
||||||
|
alert(`Error ${response.status}: ${await response.text()}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "principal", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "addr_id", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "displayname", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "description", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "color", 2);
|
||||||
|
__decorateClass([
|
||||||
|
n$1()
|
||||||
|
], CreateCalendarForm.prototype, "timezones", 2);
|
||||||
|
CreateCalendarForm = __decorateClass([
|
||||||
|
t("create-birthday-calendar-form")
|
||||||
|
], CreateCalendarForm);
|
||||||
|
export {
|
||||||
|
CreateCalendarForm
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { S as SVG_ICON_CALENDAR, a as SVG_ICON_INTERNET, e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||||
|
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -16,19 +17,23 @@ let CreateCalendarForm = class extends i {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.user = "";
|
this.user = "";
|
||||||
this.principal = "";
|
this.dialog = e();
|
||||||
|
this.form = e();
|
||||||
|
this.timezones = [];
|
||||||
|
this.resetForm();
|
||||||
|
this.fetchTimezones();
|
||||||
|
}
|
||||||
|
resetForm() {
|
||||||
|
this.form.value?.reset();
|
||||||
|
this.principal = this.user;
|
||||||
this.cal_id = self.crypto.randomUUID();
|
this.cal_id = self.crypto.randomUUID();
|
||||||
this.displayname = "";
|
this.displayname = "";
|
||||||
this.description = "";
|
this.description = "";
|
||||||
this.timezone_id = "";
|
this.timezone_id = "";
|
||||||
this.color = "";
|
this.color = "";
|
||||||
this.isSubscription = false;
|
this.isSubscription = false;
|
||||||
this.subscriptionUrl = "";
|
this.subscriptionUrl = null;
|
||||||
this.components = /* @__PURE__ */ new Set();
|
this.components = /* @__PURE__ */ new Set(["VEVENT", "VTODO"]);
|
||||||
this.dialog = e();
|
|
||||||
this.form = e();
|
|
||||||
this.timezones = [];
|
|
||||||
this.fetchTimezones();
|
|
||||||
}
|
}
|
||||||
async fetchTimezones() {
|
async fetchTimezones() {
|
||||||
this.timezones = await getTimezones();
|
this.timezones = await getTimezones();
|
||||||
@@ -38,13 +43,13 @@ let CreateCalendarForm = class extends i {
|
|||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
return x`
|
return x`
|
||||||
<button @click=${() => this.dialog.value.showModal()}>Create calendar</button>
|
<button @click=${(e2) => this.dialog.value.showModal()}>Create calendar</button>
|
||||||
<dialog ${n(this.dialog)}>
|
<dialog ${n(this.dialog)} @close=${(e2) => this.resetForm()}>
|
||||||
<h3>Create calendar</h3>
|
<h3>Create calendar</h3>
|
||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group calendars)
|
principal (for group calendars)
|
||||||
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
<select required value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map((membership) => x`
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
<option value=${membership}>${membership}</option>
|
<option value=${membership}>${membership}</option>
|
||||||
@@ -54,17 +59,17 @@ let CreateCalendarForm = class extends i {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
<input type="text" required .value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
<input type="text" required .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
<select .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||||
<option value="">No timezone</option>
|
<option value="">No timezone</option>
|
||||||
${this.timezones.map((timezone) => x`
|
${this.timezones.map((timezone) => x`
|
||||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
@@ -74,48 +79,59 @@ let CreateCalendarForm = class extends i {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" @change=${(e2) => this.description = e2.target.value} />
|
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Color
|
Color
|
||||||
<input type="color" name="color" @change=${(e2) => this.color = e2.target.value} />
|
<input type="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>Type</label>
|
||||||
Calendar is subscription to external calendar
|
<div class="tab-radio">
|
||||||
<input type="checkbox" name="is_subscription" @change=${(e2) => this.isSubscription = e2.target.checked} />
|
<label>
|
||||||
</label>
|
<input type="radio" name="type" .checked=${!this.isSubscription} @change=${(e2) => this.isSubscription = false}></input>
|
||||||
|
${SVG_ICON_CALENDAR}
|
||||||
|
Calendar
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="type" .checked=${this.isSubscription} @change=${(e2) => this.isSubscription = true}></input>
|
||||||
|
${SVG_ICON_INTERNET}
|
||||||
|
webCal Subscription
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
${this.isSubscription ? x`
|
${this.isSubscription ? x`
|
||||||
<label>
|
<label>
|
||||||
Subscription URL
|
Subscription URL
|
||||||
<input type="text" name="subscription_url" @change=${(e2) => this.subscriptionUrl = e2.target.value} />
|
<input type="text" pattern="https://.*" .required=${this.isSubscription} .value=${this.subscriptionUrl} @change=${(e2) => this.subscriptionUrl = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
|
<br>
|
||||||
` : x``}
|
` : x``}
|
||||||
<br>
|
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
<label>Components</label>
|
||||||
<label>
|
<div>
|
||||||
Support ${comp}
|
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||||
<input type="checkbox" value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
<label>
|
||||||
</label>
|
Support ${comp}
|
||||||
<br>
|
<input type="checkbox" .value=${comp} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} .checked=${this.components.has(comp)} />
|
||||||
`)}
|
</label>
|
||||||
|
<br>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Create</button>
|
<button type="submit">Create</button>
|
||||||
<button type="submit" @click=${(event) => {
|
<button type="submit" @click=${(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.dialog.value.close();
|
this.dialog.value.close();
|
||||||
this.form.value.reset();
|
|
||||||
}} class="cancel">Cancel</button>
|
}} class="cancel">Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
async submit(e2) {
|
async submit(e2) {
|
||||||
console.log(this.displayname);
|
|
||||||
e2.preventDefault();
|
e2.preventDefault();
|
||||||
if (!this.cal_id) {
|
if (!this.cal_id) {
|
||||||
alert("Empty id");
|
alert("Empty id");
|
||||||
@@ -129,6 +145,10 @@ let CreateCalendarForm = class extends i {
|
|||||||
alert("No calendar components selected");
|
alert("No calendar components selected");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this.isSubscription && !this.subscriptionUrl) {
|
||||||
|
alert("Invalid subscription url");
|
||||||
|
return;
|
||||||
|
}
|
||||||
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
|
let response = await fetch(`/caldav/principal/${this.principal || this.user}/${this.cal_id}`, {
|
||||||
method: "MKCOL",
|
method: "MKCOL",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n, t } from "./property-B8WoKf1Y.mjs";
|
import { n, t } from "./property-C8WJQOrH.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -33,12 +33,12 @@ let EditAddressbookForm = class extends i {
|
|||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
<input type="text" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<button type="submit">Submit</button>
|
<button type="submit">Submit</button>
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
import { e as escapeXml } from "./index-fgowJCc1.mjs";
|
||||||
let timezonesPromise = null;
|
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
|
||||||
async function getTimezones() {
|
|
||||||
timezonesPromise ||= new Promise(async (resolve, reject) => {
|
|
||||||
try {
|
|
||||||
let response = await fetch("/frontend/_timezones.json");
|
|
||||||
resolve(await response.json());
|
|
||||||
} catch (e2) {
|
|
||||||
reject(e2);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return await timezonesPromise;
|
|
||||||
}
|
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -51,12 +40,12 @@ let EditCalendarForm = class extends i {
|
|||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
Displayname
|
Displayname
|
||||||
<input type="text" name="displayname" .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
<input type="text" required .value=${this.displayname} @change=${(e2) => this.displayname = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Timezone (optional)
|
Timezone (optional)
|
||||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
<select .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||||
<option value="">No timezone</option>
|
<option value="">No timezone</option>
|
||||||
${this.timezones.map((timezone) => x`
|
${this.timezones.map((timezone) => x`
|
||||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||||
@@ -66,18 +55,18 @@ let EditCalendarForm = class extends i {
|
|||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Description
|
Description
|
||||||
<input type="text" name="description" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
<input type="text" .value=${this.description} @change=${(e2) => this.description = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
Color
|
Color
|
||||||
<input type="color" name="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
<input type="color" .value=${this.color} @change=${(e2) => this.color = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
${["VEVENT", "VTODO", "VJOURNAL"].map((comp) => x`
|
||||||
<label>
|
<label>
|
||||||
Support ${comp}
|
Support ${comp}
|
||||||
<input type="checkbox" value=${comp} ?checked=${this.components.has(comp)} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
<input type="checkbox" .value=${comp} ?checked=${this.components.has(comp)} @change=${(e2) => e2.target.checked ? this.components.add(e2.target.value) : this.components.delete(e2.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
`)}
|
`)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -30,23 +30,25 @@ let ImportAddressbookForm = class extends i {
|
|||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group addressbook)
|
principal (for group addressbook)
|
||||||
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
<select name="principal" required .value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option .value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map((membership) => x`
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
<option value=${membership}>${membership}</option>
|
<option .value=${membership}>${membership}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.addressbook_id} @change=${(e2) => this.addressbook_id = e2.target.value} />
|
<input type="text" required .value=${this.addressbook_id} @change=${(e2) => this.addressbook_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
file
|
file
|
||||||
<input type="file" accept="text/vcard" name="file" @change=${(e2) => this.file = e2.target.files[0]} />
|
<input type="file" accept="text/vcard" required @change=${(e2) => this.file = e2.target.files[0]} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
<button type="submit">Import</button>
|
<button type="submit">Import</button>
|
||||||
<button type="submit" @click=${(event) => {
|
<button type="submit" @click=${(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { i, x } from "./lit-DkXrt_Iv.mjs";
|
import { i, x } from "./lit-DKg0et_P.mjs";
|
||||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
import { n as n$1, t } from "./property-C8WJQOrH.mjs";
|
||||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
import { e, n } from "./ref-BivNNNRN.mjs";
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __decorateClass = (decorators, target, key, kind) => {
|
var __decorateClass = (decorators, target, key, kind) => {
|
||||||
@@ -30,23 +30,25 @@ let ImportCalendarForm = class extends i {
|
|||||||
<form @submit=${this.submit} ${n(this.form)}>
|
<form @submit=${this.submit} ${n(this.form)}>
|
||||||
<label>
|
<label>
|
||||||
principal (for group calendars)
|
principal (for group calendars)
|
||||||
<select name="principal" value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
<select name="principal" required .value=${this.user} @change=${(e2) => this.principal = e2.target.value}>
|
||||||
<option value=${this.user}>${this.user}</option>
|
<option .value=${this.user}>${this.user}</option>
|
||||||
${window.rusticalUser.memberships.map((membership) => x`
|
${window.rusticalUser.memberships.map((membership) => x`
|
||||||
<option value=${membership}>${membership}</option>
|
<option .value=${membership}>${membership}</option>
|
||||||
`)}
|
`)}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
id
|
id
|
||||||
<input type="text" name="id" value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
<input type="text" required .value=${this.cal_id} @change=${(e2) => this.cal_id = e2.target.value} />
|
||||||
</label>
|
</label>
|
||||||
<br>
|
<br>
|
||||||
<label>
|
<label>
|
||||||
file
|
file
|
||||||
<input type="file" accept="text/calendar" name="file" @change=${(e2) => this.file = e2.target.files[0]} />
|
<input type="file" required accept="text/calendar" @change=${(e2) => this.file = e2.target.files[0]} />
|
||||||
</label>
|
</label>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
<button type="submit">Import</button>
|
<button type="submit">Import</button>
|
||||||
<button type="submit" @click=${(event) => {
|
<button type="submit" @click=${(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
function escapeXml(unsafe) {
|
|
||||||
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
||||||
}
|
|
||||||
export {
|
|
||||||
escapeXml as e
|
|
||||||
};
|
|
||||||
27
crates/frontend/public/assets/js/index-fgowJCc1.mjs
Normal file
27
crates/frontend/public/assets/js/index-fgowJCc1.mjs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { b } from "./lit-DKg0et_P.mjs";
|
||||||
|
function escapeXml(unsafe) {
|
||||||
|
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
const SVG_ICON_CALENDAR = b`<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon">
|
||||||
|
<path d="M15 4V2M15 4V6M15 4H10.5M3 10V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V10H3Z" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M3 10V6C3 4.89543 3.89543 4 5 4H7" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M7 2V6" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M21 10V6C21 4.89543 20.1046 4 19 4H18.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
const SVG_ICON_INTERNET = b`<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<svg class="icon" width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M13 2.04932C13 2.04932 16 5.99994 16 11.9999" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M11 21.9506C11 21.9506 8 17.9999 8 11.9999C8 5.99994 11 2.04932 11 2.04932" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.62964 15.5H12" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.62964 8.5H21.3704" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.8789 17.9174C22.3727 18.2211 22.3423 18.9604 21.8337 19.0181L19.2671 19.309L18.1159 21.6213C17.8878 22.0795 17.1827 21.8552 17.0661 21.2873L15.8108 15.1713C15.7123 14.6913 16.1437 14.3892 16.561 14.646L21.8789 17.9174Z"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
export {
|
||||||
|
SVG_ICON_CALENDAR as S,
|
||||||
|
SVG_ICON_INTERNET as a,
|
||||||
|
escapeXml as e
|
||||||
|
};
|
||||||
@@ -66,7 +66,7 @@ const { is: i$2, defineProperty: e$1, getOwnPropertyDescriptor: h$1, getOwnPrope
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return i2;
|
return i2;
|
||||||
} }, f$1 = (t2, s2) => !i$2(t2, s2), b = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
|
} }, f$1 = (t2, s2) => !i$2(t2, s2), b$1 = { attribute: true, type: String, converter: u$1, reflect: false, useDefault: false, hasChanged: f$1 };
|
||||||
Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
|
Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap();
|
||||||
let y$1 = class y extends HTMLElement {
|
let y$1 = class y extends HTMLElement {
|
||||||
static addInitializer(t2) {
|
static addInitializer(t2) {
|
||||||
@@ -75,7 +75,7 @@ let y$1 = class y extends HTMLElement {
|
|||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
return this.finalize(), this._$Eh && [...this._$Eh.keys()];
|
||||||
}
|
}
|
||||||
static createProperty(t2, s2 = b) {
|
static createProperty(t2, s2 = b$1) {
|
||||||
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
|
if (s2.state && (s2.attribute = false), this._$Ei(), this.prototype.hasOwnProperty(t2) && ((s2 = Object.create(s2)).wrapped = true), this.elementProperties.set(t2, s2), !s2.noAccessor) {
|
||||||
const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
|
const i2 = Symbol(), h2 = this.getPropertyDescriptor(t2, i2, s2);
|
||||||
void 0 !== h2 && e$1(this.prototype, t2, h2);
|
void 0 !== h2 && e$1(this.prototype, t2, h2);
|
||||||
@@ -93,7 +93,7 @@ let y$1 = class y extends HTMLElement {
|
|||||||
}, configurable: true, enumerable: true };
|
}, configurable: true, enumerable: true };
|
||||||
}
|
}
|
||||||
static getPropertyOptions(t2) {
|
static getPropertyOptions(t2) {
|
||||||
return this.elementProperties.get(t2) ?? b;
|
return this.elementProperties.get(t2) ?? b$1;
|
||||||
}
|
}
|
||||||
static _$Ei() {
|
static _$Ei() {
|
||||||
if (this.hasOwnProperty(d$1("elementProperties"))) return;
|
if (this.hasOwnProperty(d$1("elementProperties"))) return;
|
||||||
@@ -256,7 +256,7 @@ y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$1("eleme
|
|||||||
* SPDX-License-Identifier: BSD-3-Clause
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
*/
|
*/
|
||||||
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof t2?.[Symbol.iterator], d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
|
const t = globalThis, i$1 = t.trustedTypes, s$1 = i$1 ? i$1.createPolicy("lit-html", { createHTML: (t2) => t2 }) : void 0, e = "$lit$", h = `lit$${Math.random().toFixed(9).slice(2)}$`, o$1 = "?" + h, n2 = `<${o$1}>`, r = document, l = () => r.createComment(""), c = (t2) => null === t2 || "object" != typeof t2 && "function" != typeof t2, a = Array.isArray, u = (t2) => a(t2) || "function" == typeof t2?.[Symbol.iterator], d = "[ \n\f\r]", f = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g, v = /-->/g, _ = />/g, m = RegExp(`>|${d}(?:([^\\s"'>=/]+)(${d}*=${d}*(?:[^
|
||||||
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
|
\f\r"'\`<>=]|("|')|))|$)`, "g"), p = /'/g, g = /"/g, $ = /^(?:script|style|textarea|title)$/i, y2 = (t2) => (i2, ...s2) => ({ _$litType$: t2, strings: i2, values: s2 }), x = y2(1), b = y2(2), T = Symbol.for("lit-noChange"), E = Symbol.for("lit-nothing"), A = /* @__PURE__ */ new WeakMap(), C = r.createTreeWalker(r, 129);
|
||||||
function P(t2, i2) {
|
function P(t2, i2) {
|
||||||
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
if (!a(t2) || !t2.hasOwnProperty("raw")) throw Error("invalid template strings array");
|
||||||
return void 0 !== s$1 ? s$1.createHTML(i2) : i2;
|
return void 0 !== s$1 ? s$1.createHTML(i2) : i2;
|
||||||
@@ -513,6 +513,7 @@ o?.({ LitElement: i });
|
|||||||
(s.litElementVersions ??= []).push("4.2.1");
|
(s.litElementVersions ??= []).push("4.2.1");
|
||||||
export {
|
export {
|
||||||
E,
|
E,
|
||||||
|
b,
|
||||||
f$1 as f,
|
f$1 as f,
|
||||||
i,
|
i,
|
||||||
u$1 as u,
|
u$1 as u,
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { f, u } from "./lit-DkXrt_Iv.mjs";
|
import { f, u } from "./lit-DKg0et_P.mjs";
|
||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2017 Google LLC
|
* Copyright 2017 Google LLC
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { E } from "./lit-DkXrt_Iv.mjs";
|
import { E } from "./lit-DKg0et_P.mjs";
|
||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2020 Google LLC
|
* Copyright 2020 Google LLC
|
||||||
15
crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs
Normal file
15
crates/frontend/public/assets/js/timezones-B0vBBzCP.mjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
let timezonesPromise = null;
|
||||||
|
async function getTimezones() {
|
||||||
|
timezonesPromise ||= new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
let response = await fetch("/frontend/_timezones.json");
|
||||||
|
resolve(await response.json());
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return await timezonesPromise;
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
getTimezones as g
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -53,6 +53,11 @@ a {
|
|||||||
color: var(--text-on-background-color);
|
color: var(--text-on-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
header {
|
header {
|
||||||
background: var(--background-darker);
|
background: var(--background-darker);
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
@@ -239,10 +244,8 @@ ul.collection-list {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
a,
|
|
||||||
button {
|
button {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
@@ -282,6 +285,7 @@ ul.collection-list {
|
|||||||
grid-area: actions;
|
grid-area: actions;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -334,8 +338,10 @@ select {
|
|||||||
form {
|
form {
|
||||||
|
|
||||||
input[type="text"],
|
input[type="text"],
|
||||||
|
input[type="url"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="color"],
|
input[type="color"],
|
||||||
|
input[type="file"],
|
||||||
textarea,
|
textarea,
|
||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -360,3 +366,57 @@ svg.icon {
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-radio {
|
||||||
|
display: flex;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column;
|
||||||
|
grid-auto-columns: 1fr;
|
||||||
|
|
||||||
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-on-background-color);
|
||||||
|
margin: 2px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
padding: 10px;
|
||||||
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 10%);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
appearance: none;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label:hover {
|
||||||
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input:focus-visible) {
|
||||||
|
outline: medium auto currentColor;
|
||||||
|
outline: medium auto invert;
|
||||||
|
outline: 5px auto -webkit-focus-ring-color;
|
||||||
|
|
||||||
|
input {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
label:has(input:checked) {
|
||||||
|
background: color-mix(in srgb, var(--background-color), var(--dilute-color) 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<h2>{{user.id }}'s Addressbooks</h2>
|
<h2>{{user.id }}'s Addressbooks</h2>
|
||||||
<ul class="collection-list">
|
<ul class="collection-list">
|
||||||
{% for (meta, addressbook) in addressbooks %}
|
{% for (meta, birthday_cal, addressbook) in addressbooks %}
|
||||||
<li class="collection-list-item">
|
<li class="collection-list-item">
|
||||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
@@ -24,6 +24,13 @@
|
|||||||
></edit-addressbook-form>
|
></edit-addressbook-form>
|
||||||
<delete-button trash
|
<delete-button trash
|
||||||
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
href="/carddav/principal/{{ addressbook.principal }}/{{ addressbook.id }}"></delete-button>
|
||||||
|
{% if !birthday_cal.is_some() %}
|
||||||
|
<create-birthday-calendar-form
|
||||||
|
principal="{{ addressbook.principal }}"
|
||||||
|
addr_id="{{ addressbook.id }}"
|
||||||
|
displayname="{{ addressbook.displayname.as_deref().unwrap_or_default() }} birthdays"
|
||||||
|
></create-birthday-calendar-form>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||||
@@ -37,7 +44,7 @@
|
|||||||
{%if !deleted_addressbooks.is_empty() %}
|
{%if !deleted_addressbooks.is_empty() %}
|
||||||
<h3>Deleted Addressbooks</h3>
|
<h3>Deleted Addressbooks</h3>
|
||||||
<ul class="collection-list">
|
<ul class="collection-list">
|
||||||
{% for (meta, addressbook) in deleted_addressbooks %}
|
{% for (meta, birthday_cal, addressbook) in deleted_addressbooks %}
|
||||||
<li class="collection-list-item">
|
<li class="collection-list-item">
|
||||||
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
<a href="/frontend/user/{{ addressbook.principal }}/addressbook/{{ addressbook.id}}"></a>
|
||||||
<div class="inner">
|
<div class="inner">
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
<form action="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}" target="_blank" method="GET">
|
||||||
<button type="submit">Download</button>
|
<button type="submit">Download</button>
|
||||||
</form>
|
</form>
|
||||||
{% if !calendar.id.starts_with("_birthdays_") %}
|
|
||||||
<edit-calendar-form
|
<edit-calendar-form
|
||||||
principal="{{ calendar.principal }}"
|
principal="{{ calendar.principal }}"
|
||||||
cal_id="{{ calendar.id }}"
|
cal_id="{{ calendar.id }}"
|
||||||
@@ -35,7 +34,6 @@
|
|||||||
components="{{ calendar.components | json }}"
|
components="{{ calendar.components | json }}"
|
||||||
></edit-calendar-form>
|
></edit-calendar-form>
|
||||||
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
{{ meta.len }} ({{ meta.size | filesizeformat }}) objects, {{ meta.deleted_len }} ({{ meta.deleted_size | filesizeformat }}) deleted objects
|
||||||
|
|||||||
9
crates/frontend/public/templates/icons/internet.svg
Normal file
9
crates/frontend/public/templates/icons/internet.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- Adapted from https://iconoir.com/ -->
|
||||||
|
<svg class="icon" width="24px" height="24px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M13 2.04932C13 2.04932 16 5.99994 16 11.9999" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M11 21.9506C11 21.9506 8 17.9999 8 11.9999C8 5.99994 11 2.04932 11 2.04932" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.62964 15.5H12" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M2.62964 8.5H21.3704" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.8789 17.9174C22.3727 18.2211 22.3423 18.9604 21.8337 19.0181L19.2671 19.309L18.1159 21.6213C17.8878 22.0795 17.1827 21.8552 17.0661 21.2873L15.8108 15.1713C15.7123 14.6913 16.1437 14.3892 16.561 14.646L21.8789 17.9174Z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -5,6 +5,7 @@
|
|||||||
<script>
|
<script>
|
||||||
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
|
window.rusticalUser = JSON.parse(document.querySelector('#data-rustical-user').innerHTML)
|
||||||
</script>
|
</script>
|
||||||
|
<script type="module" src="/frontend/assets/js/create-birthday-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/create-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/edit-calendar-form.mjs" async></script>
|
||||||
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>
|
<script type="module" src="/frontend/assets/js/import-calendar-form.mjs" async></script>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use http::{Method, StatusCode};
|
|||||||
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
use routes::{addressbooks::route_addressbooks, calendars::route_calendars};
|
||||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
AddressbookStore, CalendarStore,
|
AddressbookStore, CalendarStore, PrefixedCalendarStore,
|
||||||
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -39,7 +39,11 @@ use crate::routes::{
|
|||||||
#[cfg(not(feature = "dev"))]
|
#[cfg(not(feature = "dev"))]
|
||||||
use assets::{Assets, EmbedService};
|
use assets::{Assets, EmbedService};
|
||||||
|
|
||||||
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
pub fn frontend_router<
|
||||||
|
AP: AuthenticationProvider,
|
||||||
|
CS: CalendarStore,
|
||||||
|
AS: AddressbookStore + PrefixedCalendarStore,
|
||||||
|
>(
|
||||||
prefix: &'static str,
|
prefix: &'static str,
|
||||||
auth_provider: Arc<AP>,
|
auth_provider: Arc<AP>,
|
||||||
cal_store: Arc<CS>,
|
cal_store: Arc<CS>,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use askama_web::WebTemplate;
|
use askama_web::WebTemplate;
|
||||||
use axum::{Extension, extract::Path, response::IntoResponse};
|
use axum::{Extension, extract::Path, response::IntoResponse};
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use rustical_store::{Addressbook, AddressbookStore, CollectionMetadata, auth::Principal};
|
use rustical_store::{
|
||||||
|
Addressbook, AddressbookStore, Calendar, CollectionMetadata, PrefixedCalendarStore,
|
||||||
|
auth::Principal,
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::pages::user::{Section, UserPage};
|
use crate::pages::user::{Section, UserPage};
|
||||||
|
|
||||||
@@ -18,11 +20,11 @@ impl Section for AddressbooksSection {
|
|||||||
#[template(path = "components/sections/addressbooks_section.html")]
|
#[template(path = "components/sections/addressbooks_section.html")]
|
||||||
pub struct AddressbooksSection {
|
pub struct AddressbooksSection {
|
||||||
pub user: Principal,
|
pub user: Principal,
|
||||||
pub addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
pub addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
|
||||||
pub deleted_addressbooks: Vec<(CollectionMetadata, Addressbook)>,
|
pub deleted_addressbooks: Vec<(CollectionMetadata, Option<Calendar>, Addressbook)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn route_addressbooks<AS: AddressbookStore>(
|
pub async fn route_addressbooks<AS: AddressbookStore + PrefixedCalendarStore>(
|
||||||
Path(user_id): Path<String>,
|
Path(user_id): Path<String>,
|
||||||
Extension(addr_store): Extension<Arc<AS>>,
|
Extension(addr_store): Extension<Arc<AS>>,
|
||||||
user: Principal,
|
user: Principal,
|
||||||
@@ -43,22 +45,42 @@ pub async fn route_addressbooks<AS: AddressbookStore>(
|
|||||||
|
|
||||||
let mut addressbook_infos = vec![];
|
let mut addressbook_infos = vec![];
|
||||||
for addressbook in addressbooks {
|
for addressbook in addressbooks {
|
||||||
|
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
|
||||||
|
let birthday_cal = match addr_store
|
||||||
|
.get_calendar(&addressbook.principal, &birthday_id, true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cal) => Some(cal),
|
||||||
|
Err(rustical_store::Error::NotFound) => None,
|
||||||
|
err => Some(err.unwrap()),
|
||||||
|
};
|
||||||
addressbook_infos.push((
|
addressbook_infos.push((
|
||||||
addr_store
|
addr_store
|
||||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
birthday_cal,
|
||||||
addressbook,
|
addressbook,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut deleted_addressbook_infos = vec![];
|
let mut deleted_addressbook_infos = vec![];
|
||||||
for addressbook in deleted_addressbooks {
|
for addressbook in deleted_addressbooks {
|
||||||
|
let birthday_id = format!("{}{}", AS::PREFIX, &addressbook.id);
|
||||||
|
let birthday_cal = match addr_store
|
||||||
|
.get_calendar(&addressbook.principal, &birthday_id, true)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cal) => Some(cal),
|
||||||
|
Err(rustical_store::Error::NotFound) => None,
|
||||||
|
err => Some(err.unwrap()),
|
||||||
|
};
|
||||||
deleted_addressbook_infos.push((
|
deleted_addressbook_infos.push((
|
||||||
addr_store
|
addr_store
|
||||||
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
.addressbook_metadata(&addressbook.principal, &addressbook.id)
|
||||||
.await
|
.await
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
birthday_cal,
|
||||||
addressbook,
|
addressbook,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,5 +98,6 @@ pub trait CalendarStore: Send + Sync + 'static {
|
|||||||
object_id: &str,
|
object_id: &str,
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
|
// read_only refers to objects, metadata may still be updated
|
||||||
fn is_read_only(&self, cal_id: &str) -> bool;
|
fn is_read_only(&self, cal_id: &str) -> bool;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
|
|
||||||
combined_calendar_store::PrefixedCalendarStore,
|
|
||||||
};
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use derive_more::derive::Constructor;
|
|
||||||
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
|
|
||||||
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
|
||||||
|
|
||||||
#[derive(Constructor, Clone)]
|
|
||||||
pub struct ContactBirthdayStore<AS: AddressbookStore>(Arc<AS>);
|
|
||||||
|
|
||||||
impl<AS: AddressbookStore> PrefixedCalendarStore for ContactBirthdayStore<AS> {
|
|
||||||
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
|
||||||
Calendar {
|
|
||||||
principal: addressbook.principal,
|
|
||||||
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
|
|
||||||
meta: CalendarMetadata {
|
|
||||||
displayname: addressbook
|
|
||||||
.displayname
|
|
||||||
.map(|name| format!("{name} birthdays")),
|
|
||||||
order: 0,
|
|
||||||
description: None,
|
|
||||||
color: None,
|
|
||||||
},
|
|
||||||
timezone_id: None,
|
|
||||||
deleted_at: addressbook.deleted_at,
|
|
||||||
synctoken: addressbook.synctoken,
|
|
||||||
subscription_url: None,
|
|
||||||
push_topic: {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update("birthdays");
|
|
||||||
hasher.update(addressbook.push_topic);
|
|
||||||
format!("{:x}", hasher.finalize())
|
|
||||||
},
|
|
||||||
components: vec![CalendarObjectType::Event],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Objects are all prefixed with `BIRTHDAYS_PREFIX`
|
|
||||||
#[async_trait]
|
|
||||||
impl<AS: AddressbookStore> CalendarStore for ContactBirthdayStore<AS> {
|
|
||||||
async fn get_calendar(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
id: &str,
|
|
||||||
show_deleted: bool,
|
|
||||||
) -> Result<Calendar, Error> {
|
|
||||||
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
|
|
||||||
let addressbook = self.0.get_addressbook(principal, id, show_deleted).await?;
|
|
||||||
Ok(birthday_calendar(addressbook))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
|
||||||
let addressbooks = self.0.get_addressbooks(principal).await?;
|
|
||||||
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
|
||||||
let addressbooks = self.0.get_deleted_addressbooks(principal).await?;
|
|
||||||
Ok(addressbooks.into_iter().map(birthday_calendar).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_calendar(
|
|
||||||
&self,
|
|
||||||
_principal: String,
|
|
||||||
_id: String,
|
|
||||||
_calendar: Calendar,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_calendar(&self, _calendar: Calendar) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
async fn delete_calendar(
|
|
||||||
&self,
|
|
||||||
_principal: &str,
|
|
||||||
_name: &str,
|
|
||||||
_use_trashbin: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_calendar(&self, _principal: &str, _name: &str) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn import_calendar(
|
|
||||||
&self,
|
|
||||||
_calendar: Calendar,
|
|
||||||
_objects: Vec<CalendarObject>,
|
|
||||||
_merge_existing: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sync_changes(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
synctoken: i64,
|
|
||||||
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
|
||||||
let cal_id = cal_id
|
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
let (objects, deleted_objects, new_synctoken) =
|
|
||||||
self.0.sync_changes(principal, cal_id, synctoken).await?;
|
|
||||||
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
|
|
||||||
.iter()
|
|
||||||
.map(AddressObject::get_birthday_object)
|
|
||||||
.collect();
|
|
||||||
let objects = objects?.into_iter().flatten().collect();
|
|
||||||
|
|
||||||
Ok((objects, deleted_objects, new_synctoken))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn calendar_metadata(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
) -> Result<crate::CollectionMetadata, Error> {
|
|
||||||
let cal_id = cal_id
|
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
self.0.addressbook_metadata(principal, cal_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_objects(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
) -> Result<Vec<CalendarObject>, Error> {
|
|
||||||
let cal_id = cal_id
|
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
|
|
||||||
self.0
|
|
||||||
.get_objects(principal, cal_id)
|
|
||||||
.await?
|
|
||||||
.iter()
|
|
||||||
.map(AddressObject::get_significant_dates)
|
|
||||||
.collect();
|
|
||||||
let objects = objects?
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(HashMap::into_values)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_object(
|
|
||||||
&self,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
object_id: &str,
|
|
||||||
show_deleted: bool,
|
|
||||||
) -> Result<CalendarObject, Error> {
|
|
||||||
let cal_id = cal_id
|
|
||||||
.strip_prefix(BIRTHDAYS_PREFIX)
|
|
||||||
.ok_or(Error::NotFound)?;
|
|
||||||
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
|
|
||||||
self.0
|
|
||||||
.get_object(principal, cal_id, addressobject_id, show_deleted)
|
|
||||||
.await?
|
|
||||||
.get_significant_dates()?
|
|
||||||
.remove(date_type)
|
|
||||||
.ok_or(Error::NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn put_object(
|
|
||||||
&self,
|
|
||||||
_principal: String,
|
|
||||||
_cal_id: String,
|
|
||||||
_object: CalendarObject,
|
|
||||||
_overwrite: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_object(
|
|
||||||
&self,
|
|
||||||
_principal: &str,
|
|
||||||
_cal_id: &str,
|
|
||||||
_object_id: &str,
|
|
||||||
_use_trashbin: bool,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_object(
|
|
||||||
&self,
|
|
||||||
_principal: &str,
|
|
||||||
_cal_id: &str,
|
|
||||||
_object_id: &str,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
Err(Error::ReadOnly)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_read_only(&self, _cal_id: &str) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@ pub use error::Error;
|
|||||||
pub mod auth;
|
pub mod auth;
|
||||||
mod calendar;
|
mod calendar;
|
||||||
mod combined_calendar_store;
|
mod combined_calendar_store;
|
||||||
mod contact_birthday_store;
|
|
||||||
mod secret;
|
mod secret;
|
||||||
mod subscription_store;
|
mod subscription_store;
|
||||||
pub mod synctoken;
|
pub mod synctoken;
|
||||||
@@ -17,8 +16,7 @@ pub mod tests;
|
|||||||
|
|
||||||
pub use addressbook_store::AddressbookStore;
|
pub use addressbook_store::AddressbookStore;
|
||||||
pub use calendar_store::CalendarStore;
|
pub use calendar_store::CalendarStore;
|
||||||
pub use combined_calendar_store::CombinedCalendarStore;
|
pub use combined_calendar_store::{CombinedCalendarStore, PrefixedCalendarStore};
|
||||||
pub use contact_birthday_store::ContactBirthdayStore;
|
|
||||||
pub use secret::Secret;
|
pub use secret::Secret;
|
||||||
pub use subscription_store::*;
|
pub use subscription_store::*;
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ uuid.workspace = true
|
|||||||
pbkdf2.workspace = true
|
pbkdf2.workspace = true
|
||||||
rustical_ical.workspace = true
|
rustical_ical.workspace = true
|
||||||
rstest = { workspace = true, optional = true }
|
rstest = { workspace = true, optional = true }
|
||||||
|
sha2.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE birthday_calendars;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
CREATE TABLE birthday_calendars (
|
||||||
|
principal TEXT NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
displayname TEXT,
|
||||||
|
description TEXT,
|
||||||
|
"order" INT DEFAULT 0 NOT NULL,
|
||||||
|
color TEXT,
|
||||||
|
timezone_id TEXT,
|
||||||
|
deleted_at DATETIME,
|
||||||
|
push_topic TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (principal, id),
|
||||||
|
CONSTRAINT fk_birthdays_addressbooks FOREIGN KEY (principal, id)
|
||||||
|
REFERENCES addressbooks (principal, id) ON DELETE CASCADE
|
||||||
|
-- birthday calendar stores no meaningful data so we can cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO birthday_calendars
|
||||||
|
(principal, id, displayname, deleted_at, push_topic)
|
||||||
|
SELECT
|
||||||
|
principal,
|
||||||
|
id,
|
||||||
|
displayname || ' birthdays' AS displayname,
|
||||||
|
deleted_at,
|
||||||
|
push_topic || substr(printf('%d', random()), -4) AS push_topic
|
||||||
|
-- jank suffix to ensure that new push_topic is different :D
|
||||||
|
FROM addressbooks;
|
||||||
431
crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
Normal file
431
crates/store_sqlite/src/addressbook_store/birthday_calendar.rs
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
use crate::addressbook_store::SqliteAddressbookStore;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
|
||||||
|
use rustical_store::{
|
||||||
|
Addressbook, AddressbookStore, Calendar, CalendarMetadata, CalendarStore, CollectionMetadata,
|
||||||
|
Error, PrefixedCalendarStore,
|
||||||
|
};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use sqlx::{Executor, Sqlite};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
pub const BIRTHDAYS_PREFIX: &str = "_birthdays_";
|
||||||
|
|
||||||
|
struct BirthdayCalendarJoinRow {
|
||||||
|
principal: String,
|
||||||
|
id: String,
|
||||||
|
displayname: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
order: i64,
|
||||||
|
color: Option<String>,
|
||||||
|
timezone_id: Option<String>,
|
||||||
|
deleted_at: Option<NaiveDateTime>,
|
||||||
|
push_topic: String,
|
||||||
|
|
||||||
|
addr_synctoken: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BirthdayCalendarJoinRow> for Calendar {
|
||||||
|
fn from(value: BirthdayCalendarJoinRow) -> Self {
|
||||||
|
Self {
|
||||||
|
principal: value.principal,
|
||||||
|
id: format!("{}{}", BIRTHDAYS_PREFIX, value.id),
|
||||||
|
meta: CalendarMetadata {
|
||||||
|
displayname: value.displayname,
|
||||||
|
order: value.order,
|
||||||
|
description: value.description,
|
||||||
|
color: value.color,
|
||||||
|
},
|
||||||
|
deleted_at: value.deleted_at,
|
||||||
|
components: vec![CalendarObjectType::Event],
|
||||||
|
timezone_id: value.timezone_id,
|
||||||
|
synctoken: value.addr_synctoken,
|
||||||
|
subscription_url: None,
|
||||||
|
push_topic: value.push_topic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrefixedCalendarStore for SqliteAddressbookStore {
|
||||||
|
const PREFIX: &'static str = BIRTHDAYS_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqliteAddressbookStore {
|
||||||
|
#[instrument]
|
||||||
|
pub async fn _get_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<Calendar, Error> {
|
||||||
|
let cal = sqlx::query_as!(
|
||||||
|
BirthdayCalendarJoinRow,
|
||||||
|
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
|
||||||
|
FROM birthday_calendars
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT principal AS addr_principal,
|
||||||
|
id AS addr_id,
|
||||||
|
synctoken AS addr_synctoken
|
||||||
|
FROM addressbooks
|
||||||
|
) ON (principal, id) = (addr_principal, addr_id)
|
||||||
|
WHERE (principal, id) = (?, ?)
|
||||||
|
AND ((deleted_at IS NULL) OR ?)
|
||||||
|
"#,
|
||||||
|
principal,
|
||||||
|
id,
|
||||||
|
show_deleted
|
||||||
|
)
|
||||||
|
.fetch_one(executor)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
Ok(cal.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub async fn _get_birthday_calendars<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
deleted: bool,
|
||||||
|
) -> Result<Vec<Calendar>, Error> {
|
||||||
|
Ok(
|
||||||
|
sqlx::query_as!(
|
||||||
|
BirthdayCalendarJoinRow,
|
||||||
|
r#"SELECT principal, id, displayname, description, "order", color, timezone_id, deleted_at, addr_synctoken, push_topic
|
||||||
|
FROM birthday_calendars
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT principal AS addr_principal,
|
||||||
|
id AS addr_id,
|
||||||
|
synctoken AS addr_synctoken
|
||||||
|
FROM addressbooks
|
||||||
|
) ON (principal, id) = (addr_principal, addr_id)
|
||||||
|
WHERE principal = ?
|
||||||
|
AND (
|
||||||
|
(deleted_at IS NULL AND NOT ?) -- not deleted, want not deleted
|
||||||
|
OR (deleted_at IS NOT NULL AND ?) -- deleted, want deleted
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
principal,
|
||||||
|
deleted,
|
||||||
|
deleted
|
||||||
|
)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from).map(|cals| cals.into_iter().map(BirthdayCalendarJoinRow::into).collect())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn default_birthday_calendar(addressbook: Addressbook) -> Calendar {
|
||||||
|
let birthday_name = addressbook
|
||||||
|
.displayname
|
||||||
|
.as_ref()
|
||||||
|
.map(|name| format!("{name} birthdays"));
|
||||||
|
let birthday_push_topic = {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update("birthdays");
|
||||||
|
hasher.update(&addressbook.push_topic);
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
};
|
||||||
|
Calendar {
|
||||||
|
principal: addressbook.principal,
|
||||||
|
meta: CalendarMetadata {
|
||||||
|
displayname: birthday_name,
|
||||||
|
order: 0,
|
||||||
|
description: None,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
|
id: format!("{}{}", Self::PREFIX, addressbook.id),
|
||||||
|
components: vec![CalendarObjectType::Event],
|
||||||
|
timezone_id: None,
|
||||||
|
deleted_at: None,
|
||||||
|
synctoken: Default::default(),
|
||||||
|
subscription_url: None,
|
||||||
|
push_topic: birthday_push_topic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub async fn _insert_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
calendar: &Calendar,
|
||||||
|
) -> Result<(), rustical_store::Error> {
|
||||||
|
let id = calendar
|
||||||
|
.id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"INSERT INTO birthday_calendars (principal, id, displayname, description, "order", color, push_topic)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
|
calendar.principal,
|
||||||
|
id,
|
||||||
|
calendar.meta.displayname,
|
||||||
|
calendar.meta.description,
|
||||||
|
calendar.meta.order,
|
||||||
|
calendar.meta.color,
|
||||||
|
calendar.push_topic,
|
||||||
|
)
|
||||||
|
.execute(executor)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _delete_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
use_trashbin: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if use_trashbin {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"UPDATE birthday_calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
|
||||||
|
principal,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(executor)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"DELETE FROM birthday_calendars WHERE (principal, id) = (?, ?)"#,
|
||||||
|
principal,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(executor)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn _restore_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
r"UPDATE birthday_calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
|
||||||
|
principal,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(executor)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn _update_birthday_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
|
executor: E,
|
||||||
|
principal: &str,
|
||||||
|
calendar: &Calendar,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
r#"UPDATE birthday_calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?
|
||||||
|
WHERE (principal, id) = (?, ?)"#,
|
||||||
|
calendar.principal,
|
||||||
|
calendar.id,
|
||||||
|
calendar.meta.displayname,
|
||||||
|
calendar.meta.description,
|
||||||
|
calendar.meta.order,
|
||||||
|
calendar.meta.color,
|
||||||
|
calendar.timezone_id,
|
||||||
|
calendar.push_topic,
|
||||||
|
principal,
|
||||||
|
calendar.id,
|
||||||
|
).execute(executor).await.map_err(crate::Error::from)?;
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(rustical_store::Error::NotFound);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl CalendarStore for SqliteAddressbookStore {
|
||||||
|
#[instrument]
|
||||||
|
async fn get_calendar(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<Calendar, Error> {
|
||||||
|
let id = id.strip_prefix(BIRTHDAYS_PREFIX).ok_or(Error::NotFound)?;
|
||||||
|
Self::_get_birthday_calendar(&self.db, principal, id, show_deleted).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn get_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||||
|
Self::_get_birthday_calendars(&self.db, principal, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn get_deleted_calendars(&self, principal: &str) -> Result<Vec<Calendar>, Error> {
|
||||||
|
Self::_get_birthday_calendars(&self.db, principal, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn update_calendar(
|
||||||
|
&self,
|
||||||
|
principal: String,
|
||||||
|
id: String,
|
||||||
|
mut calendar: Calendar,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
assert_eq!(id, calendar.id);
|
||||||
|
calendar.id = calendar
|
||||||
|
.id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?
|
||||||
|
.to_string();
|
||||||
|
Self::_update_birthday_calendar(&self.db, &principal, &calendar).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn insert_calendar(&self, calendar: Calendar) -> Result<(), Error> {
|
||||||
|
Self::_insert_birthday_calendar(&self.db, &calendar).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn delete_calendar(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
id: &str,
|
||||||
|
use_trashbin: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
Self::_delete_birthday_calendar(&self.db, principal, id, use_trashbin).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn restore_calendar(&self, principal: &str, id: &str) -> Result<(), Error> {
|
||||||
|
let Some(id) = id.strip_prefix(BIRTHDAYS_PREFIX) else {
|
||||||
|
return Err(Error::NotFound);
|
||||||
|
};
|
||||||
|
Self::_restore_birthday_calendar(&self.db, principal, id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn import_calendar(
|
||||||
|
&self,
|
||||||
|
_calendar: Calendar,
|
||||||
|
_objects: Vec<CalendarObject>,
|
||||||
|
_merge_existing: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
Err(Error::ReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn sync_changes(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
synctoken: i64,
|
||||||
|
) -> Result<(Vec<CalendarObject>, Vec<String>, i64), Error> {
|
||||||
|
let cal_id = cal_id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
let (objects, deleted_objects, new_synctoken) =
|
||||||
|
AddressbookStore::sync_changes(self, principal, cal_id, synctoken).await?;
|
||||||
|
let objects: Result<Vec<Option<CalendarObject>>, rustical_ical::Error> = objects
|
||||||
|
.iter()
|
||||||
|
.map(AddressObject::get_birthday_object)
|
||||||
|
.collect();
|
||||||
|
let objects = objects?.into_iter().flatten().collect();
|
||||||
|
|
||||||
|
Ok((objects, deleted_objects, new_synctoken))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn calendar_metadata(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<CollectionMetadata, Error> {
|
||||||
|
let cal_id = cal_id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
self.addressbook_metadata(principal, cal_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn get_objects(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
) -> Result<Vec<CalendarObject>, Error> {
|
||||||
|
let cal_id = cal_id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
let objects: Result<Vec<HashMap<&'static str, CalendarObject>>, rustical_ical::Error> =
|
||||||
|
AddressbookStore::get_objects(self, principal, cal_id)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(AddressObject::get_significant_dates)
|
||||||
|
.collect();
|
||||||
|
let objects = objects?
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(HashMap::into_values)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn get_object(
|
||||||
|
&self,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
object_id: &str,
|
||||||
|
show_deleted: bool,
|
||||||
|
) -> Result<CalendarObject, Error> {
|
||||||
|
let cal_id = cal_id
|
||||||
|
.strip_prefix(BIRTHDAYS_PREFIX)
|
||||||
|
.ok_or(Error::NotFound)?;
|
||||||
|
let (addressobject_id, date_type) = object_id.rsplit_once('-').ok_or(Error::NotFound)?;
|
||||||
|
AddressbookStore::get_object(self, principal, cal_id, addressobject_id, show_deleted)
|
||||||
|
.await?
|
||||||
|
.get_significant_dates()?
|
||||||
|
.remove(date_type)
|
||||||
|
.ok_or(Error::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn put_object(
|
||||||
|
&self,
|
||||||
|
_principal: String,
|
||||||
|
_cal_id: String,
|
||||||
|
_object: CalendarObject,
|
||||||
|
_overwrite: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
Err(Error::ReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn delete_object(
|
||||||
|
&self,
|
||||||
|
_principal: &str,
|
||||||
|
_cal_id: &str,
|
||||||
|
_object_id: &str,
|
||||||
|
_use_trashbin: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
Err(Error::ReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn restore_object(
|
||||||
|
&self,
|
||||||
|
_principal: &str,
|
||||||
|
_cal_id: &str,
|
||||||
|
_object_id: &str,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
Err(Error::ReadOnly)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_read_only(&self, _cal_id: &str) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ use rustical_store::{
|
|||||||
};
|
};
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error_span, instrument, warn};
|
||||||
|
|
||||||
|
pub mod birthday_calendar;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AddressObjectRow {
|
struct AddressObjectRow {
|
||||||
@@ -32,6 +34,111 @@ pub struct SqliteAddressbookStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteAddressbookStore {
|
impl SqliteAddressbookStore {
|
||||||
|
// Commit "orphaned" objects to the changelog table
|
||||||
|
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||||
|
struct Row {
|
||||||
|
principal: String,
|
||||||
|
addressbook_id: String,
|
||||||
|
id: String,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = self
|
||||||
|
.db
|
||||||
|
.begin_with(BEGIN_IMMEDIATE)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as!(
|
||||||
|
Row,
|
||||||
|
r#"
|
||||||
|
SELECT principal, addressbook_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
|
||||||
|
FROM addressobjects
|
||||||
|
WHERE (principal, addressbook_id, id) NOT IN (
|
||||||
|
SELECT DISTINCT principal, addressbook_id, object_id FROM addressobjectchangelog
|
||||||
|
)
|
||||||
|
;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let operation = if row.deleted {
|
||||||
|
ChangeOperation::Delete
|
||||||
|
} else {
|
||||||
|
ChangeOperation::Add
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
"Commiting orphaned addressbook object ({},{},{}), deleted={}",
|
||||||
|
&row.principal, &row.addressbook_id, &row.id, &row.deleted
|
||||||
|
);
|
||||||
|
Self::log_object_operation(
|
||||||
|
&mut tx,
|
||||||
|
&row.principal,
|
||||||
|
&row.addressbook_id,
|
||||||
|
&row.id,
|
||||||
|
operation,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logs an operation to an address object
|
||||||
|
async fn log_object_operation(
|
||||||
|
tx: &mut Transaction<'_, Sqlite>,
|
||||||
|
principal: &str,
|
||||||
|
addressbook_id: &str,
|
||||||
|
object_id: &str,
|
||||||
|
operation: ChangeOperation,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
struct Synctoken {
|
||||||
|
synctoken: i64,
|
||||||
|
}
|
||||||
|
let Synctoken { synctoken } = sqlx::query_as!(
|
||||||
|
Synctoken,
|
||||||
|
r#"
|
||||||
|
UPDATE addressbooks
|
||||||
|
SET synctoken = synctoken + 1
|
||||||
|
WHERE (principal, id) = (?1, ?2)
|
||||||
|
RETURNING synctoken"#,
|
||||||
|
principal,
|
||||||
|
addressbook_id
|
||||||
|
)
|
||||||
|
.fetch_one(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO addressobjectchangelog (principal, addressbook_id, object_id, "operation", synctoken)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, (
|
||||||
|
SELECT synctoken FROM addressbooks WHERE (principal, id) = (?1, ?2)
|
||||||
|
))"#,
|
||||||
|
principal,
|
||||||
|
addressbook_id,
|
||||||
|
object_id,
|
||||||
|
operation
|
||||||
|
)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
Ok(format_synctoken(synctoken))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_push_notification(&self, data: CollectionOperationInfo, topic: String) {
|
||||||
|
if let Err(err) = self.sender.try_send(CollectionOperation { topic, data }) {
|
||||||
|
error_span!(
|
||||||
|
"Error trying to send addressbook update notification:",
|
||||||
|
err = format!("{err:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -90,9 +197,9 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
id: String,
|
id: &str,
|
||||||
addressbook: Addressbook,
|
addressbook: &Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
|
||||||
@@ -116,7 +223,7 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
addressbook: Addressbook,
|
addressbook: &Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
|
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
|
||||||
@@ -283,9 +390,9 @@ impl SqliteAddressbookStore {
|
|||||||
|
|
||||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
addressbook_id: String,
|
addressbook_id: &str,
|
||||||
object: AddressObject,
|
object: &AddressObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
let (object_id, vcf) = (object.get_id(), object.get_vcf());
|
let (object_id, vcf) = (object.get_id(), object.get_vcf());
|
||||||
@@ -397,7 +504,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
id: String,
|
id: String,
|
||||||
addressbook: Addressbook,
|
addressbook: Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
Self::_update_addressbook(&self.db, principal, id, addressbook).await
|
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -405,7 +512,16 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
&self,
|
&self,
|
||||||
addressbook: Addressbook,
|
addressbook: Addressbook,
|
||||||
) -> Result<(), rustical_store::Error> {
|
) -> Result<(), rustical_store::Error> {
|
||||||
Self::_insert_addressbook(&self.db, addressbook).await
|
let mut tx = self
|
||||||
|
.db
|
||||||
|
.begin_with(BEGIN_IMMEDIATE)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||||
|
let birthday_cal = Self::default_birthday_calendar(addressbook);
|
||||||
|
Self::_insert_birthday_calendar(&mut *tx, &birthday_cal).await?;
|
||||||
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
@@ -431,13 +547,8 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?;
|
Self::_delete_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Some(addressbook) = addressbook
|
if let Some(addressbook) = addressbook {
|
||||||
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(CollectionOperationInfo::Delete, addressbook.push_topic);
|
||||||
data: CollectionOperationInfo::Delete,
|
|
||||||
topic: addressbook.push_topic,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -521,16 +632,9 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
let object_id = object.get_id().to_owned();
|
let object_id = object.get_id().to_owned();
|
||||||
|
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?;
|
||||||
&mut *tx,
|
|
||||||
principal.clone(),
|
|
||||||
addressbook_id.clone(),
|
|
||||||
object,
|
|
||||||
overwrite,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = Self::log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&principal,
|
&principal,
|
||||||
&addressbook_id,
|
&addressbook_id,
|
||||||
@@ -542,15 +646,12 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self
|
self.get_addressbook(&principal, &addressbook_id, false)
|
||||||
.get_addressbook(&principal, &addressbook_id, false)
|
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
}) {
|
);
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -571,7 +672,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
|
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = Self::log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
@@ -583,15 +684,12 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self
|
self.get_addressbook(principal, addressbook_id, false)
|
||||||
.get_addressbook(principal, addressbook_id, false)
|
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
}) {
|
);
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +708,7 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
|
|
||||||
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
|
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = Self::log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
principal,
|
principal,
|
||||||
addressbook_id,
|
addressbook_id,
|
||||||
@@ -621,15 +719,12 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self
|
self.get_addressbook(principal, addressbook_id, false)
|
||||||
.get_addressbook(principal, addressbook_id, false)
|
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
}) {
|
);
|
||||||
error!("Push notification about deleted addressbook failed: {err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -659,61 +754,41 @@ impl AddressbookStore for SqliteAddressbookStore {
|
|||||||
return Err(Error::AlreadyExists);
|
return Err(Error::AlreadyExists);
|
||||||
}
|
}
|
||||||
if existing.is_none() {
|
if existing.is_none() {
|
||||||
Self::_insert_addressbook(&mut *tx, addressbook.clone()).await?;
|
Self::_insert_addressbook(&mut *tx, &addressbook).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for object in objects {
|
||||||
Self::_put_object(
|
Self::_put_object(
|
||||||
&mut *tx,
|
&mut *tx,
|
||||||
addressbook.principal.clone(),
|
&addressbook.principal,
|
||||||
addressbook.id.clone(),
|
&addressbook.id,
|
||||||
object,
|
&object,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sync_token = Some(
|
||||||
|
Self::log_object_operation(
|
||||||
|
&mut tx,
|
||||||
|
&addressbook.principal,
|
||||||
|
&addressbook.id,
|
||||||
|
object.get_id(),
|
||||||
|
ChangeOperation::Add,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
if let Some(sync_token) = sync_token {
|
||||||
|
self.send_push_notification(
|
||||||
|
CollectionOperationInfo::Content { sync_token },
|
||||||
|
self.get_addressbook(&addressbook.principal, &addressbook.id, true)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logs an operation to an address object
|
|
||||||
async fn log_object_operation(
|
|
||||||
tx: &mut Transaction<'_, Sqlite>,
|
|
||||||
principal: &str,
|
|
||||||
addressbook_id: &str,
|
|
||||||
object_id: &str,
|
|
||||||
operation: ChangeOperation,
|
|
||||||
) -> Result<String, sqlx::Error> {
|
|
||||||
struct Synctoken {
|
|
||||||
synctoken: i64,
|
|
||||||
}
|
|
||||||
let Synctoken { synctoken } = sqlx::query_as!(
|
|
||||||
Synctoken,
|
|
||||||
r#"
|
|
||||||
UPDATE addressbooks
|
|
||||||
SET synctoken = synctoken + 1
|
|
||||||
WHERE (principal, id) = (?1, ?2)
|
|
||||||
RETURNING synctoken"#,
|
|
||||||
principal,
|
|
||||||
addressbook_id
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **tx)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO addressobjectchangelog (principal, addressbook_id, object_id, "operation", synctoken)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, (
|
|
||||||
SELECT synctoken FROM addressbooks WHERE (principal, id) = (?1, ?2)
|
|
||||||
))"#,
|
|
||||||
principal,
|
|
||||||
addressbook_id,
|
|
||||||
object_id,
|
|
||||||
operation
|
|
||||||
)
|
|
||||||
.execute(&mut **tx)
|
|
||||||
.await?;
|
|
||||||
Ok(format_synctoken(synctoken))
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo};
|
|||||||
use sqlx::types::chrono::NaiveDateTime;
|
use sqlx::types::chrono::NaiveDateTime;
|
||||||
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tracing::{error, instrument};
|
use tracing::{error_span, instrument, warn};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct CalendarObjectRow {
|
struct CalendarObjectRow {
|
||||||
@@ -94,6 +94,105 @@ pub struct SqliteCalendarStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SqliteCalendarStore {
|
impl SqliteCalendarStore {
|
||||||
|
// Logs an operation to the events
|
||||||
|
async fn log_object_operation(
|
||||||
|
tx: &mut Transaction<'_, Sqlite>,
|
||||||
|
principal: &str,
|
||||||
|
cal_id: &str,
|
||||||
|
object_id: &str,
|
||||||
|
operation: ChangeOperation,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
struct Synctoken {
|
||||||
|
synctoken: i64,
|
||||||
|
}
|
||||||
|
let Synctoken { synctoken } = sqlx::query_as!(
|
||||||
|
Synctoken,
|
||||||
|
r#"
|
||||||
|
UPDATE calendars
|
||||||
|
SET synctoken = synctoken + 1
|
||||||
|
WHERE (principal, id) = (?1, ?2)
|
||||||
|
RETURNING synctoken"#,
|
||||||
|
principal,
|
||||||
|
cal_id
|
||||||
|
)
|
||||||
|
.fetch_one(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO calendarobjectchangelog (principal, cal_id, object_id, "operation", synctoken)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, (
|
||||||
|
SELECT synctoken FROM calendars WHERE (principal, id) = (?1, ?2)
|
||||||
|
))"#,
|
||||||
|
principal,
|
||||||
|
cal_id,
|
||||||
|
object_id,
|
||||||
|
operation
|
||||||
|
)
|
||||||
|
.execute(&mut **tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
Ok(format_synctoken(synctoken))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_push_notification(&self, data: CollectionOperationInfo, topic: String) {
|
||||||
|
if let Err(err) = self.sender.try_send(CollectionOperation { topic, data }) {
|
||||||
|
error_span!(
|
||||||
|
"Error trying to send calendar update notification:",
|
||||||
|
err = format!("{err:?}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit "orphaned" objects to the changelog table
|
||||||
|
pub async fn repair_orphans(&self) -> Result<(), Error> {
|
||||||
|
struct Row {
|
||||||
|
principal: String,
|
||||||
|
cal_id: String,
|
||||||
|
id: String,
|
||||||
|
deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tx = self
|
||||||
|
.db
|
||||||
|
.begin_with(BEGIN_IMMEDIATE)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as!(
|
||||||
|
Row,
|
||||||
|
r#"
|
||||||
|
SELECT principal, cal_id, id, (deleted_at IS NOT NULL) AS "deleted: bool"
|
||||||
|
FROM calendarobjects
|
||||||
|
WHERE (principal, cal_id, id) NOT IN (
|
||||||
|
SELECT DISTINCT principal, cal_id, object_id FROM calendarobjectchangelog
|
||||||
|
)
|
||||||
|
;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let operation = if row.deleted {
|
||||||
|
ChangeOperation::Delete
|
||||||
|
} else {
|
||||||
|
ChangeOperation::Add
|
||||||
|
};
|
||||||
|
warn!(
|
||||||
|
"Commiting orphaned calendar object ({},{},{}), deleted={}",
|
||||||
|
&row.principal, &row.cal_id, &row.id, &row.deleted
|
||||||
|
);
|
||||||
|
Self::log_object_operation(&mut tx, &row.principal, &row.cal_id, &row.id, operation)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _get_calendar<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: &str,
|
principal: &str,
|
||||||
@@ -351,9 +450,9 @@ impl SqliteCalendarStore {
|
|||||||
#[instrument]
|
#[instrument]
|
||||||
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
|
||||||
executor: E,
|
executor: E,
|
||||||
principal: String,
|
principal: &str,
|
||||||
cal_id: String,
|
cal_id: &str,
|
||||||
object: CalendarObject,
|
object: &CalendarObject,
|
||||||
overwrite: bool,
|
overwrite: bool,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
||||||
@@ -558,13 +657,8 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?;
|
Self::_delete_calendar(&mut *tx, principal, id, use_trashbin).await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Some(cal) = cal
|
if let Some(cal) = cal {
|
||||||
&& let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(CollectionOperationInfo::Delete, cal.push_topic);
|
||||||
data: CollectionOperationInfo::Delete,
|
|
||||||
topic: cal.push_topic,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -600,18 +694,32 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
|
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut sync_token = None;
|
||||||
for object in objects {
|
for object in objects {
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
|
||||||
&mut *tx,
|
|
||||||
calendar.principal.clone(),
|
sync_token = Some(
|
||||||
calendar.id.clone(),
|
Self::log_object_operation(
|
||||||
object,
|
&mut tx,
|
||||||
false,
|
&calendar.principal,
|
||||||
)
|
&calendar.id,
|
||||||
.await?;
|
object.get_id(),
|
||||||
|
ChangeOperation::Add,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
|
if let Some(sync_token) = sync_token {
|
||||||
|
self.send_push_notification(
|
||||||
|
CollectionOperationInfo::Content { sync_token },
|
||||||
|
self.get_calendar(&calendar.principal, &calendar.id, true)
|
||||||
|
.await?
|
||||||
|
.push_topic,
|
||||||
|
);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,16 +797,9 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
return Err(Error::ReadOnly);
|
return Err(Error::ReadOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::_put_object(
|
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
|
||||||
&mut *tx,
|
|
||||||
principal.clone(),
|
|
||||||
cal_id.clone(),
|
|
||||||
object,
|
|
||||||
overwrite,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let sync_token = log_object_operation(
|
let sync_token = Self::log_object_operation(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&principal,
|
&principal,
|
||||||
&cal_id,
|
&cal_id,
|
||||||
@@ -709,15 +810,12 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
|
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self
|
self.get_calendar(&principal, &cal_id, true)
|
||||||
.get_calendar(&principal, &cal_id, true)
|
|
||||||
.await?
|
.await?
|
||||||
.push_topic,
|
.push_topic,
|
||||||
}) {
|
);
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,15 +836,15 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
|
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
|
||||||
|
|
||||||
let sync_token =
|
let sync_token =
|
||||||
log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete).await?;
|
Self::log_object_operation(&mut tx, principal, cal_id, id, ChangeOperation::Delete)
|
||||||
|
.await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
self.get_calendar(principal, cal_id, true).await?.push_topic,
|
||||||
}) {
|
);
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -766,16 +864,14 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
|
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
|
||||||
|
|
||||||
let sync_token =
|
let sync_token =
|
||||||
log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add)
|
Self::log_object_operation(&mut tx, principal, cal_id, object_id, ChangeOperation::Add)
|
||||||
.await?;
|
.await?;
|
||||||
tx.commit().await.map_err(crate::Error::from)?;
|
tx.commit().await.map_err(crate::Error::from)?;
|
||||||
|
|
||||||
if let Err(err) = self.sender.try_send(CollectionOperation {
|
self.send_push_notification(
|
||||||
data: CollectionOperationInfo::Content { sync_token },
|
CollectionOperationInfo::Content { sync_token },
|
||||||
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
|
self.get_calendar(principal, cal_id, true).await?.push_topic,
|
||||||
}) {
|
);
|
||||||
error!("Push notification about deleted calendar failed: {err}");
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,45 +889,3 @@ impl CalendarStore for SqliteCalendarStore {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logs an operation to the events
|
|
||||||
async fn log_object_operation(
|
|
||||||
tx: &mut Transaction<'_, Sqlite>,
|
|
||||||
principal: &str,
|
|
||||||
cal_id: &str,
|
|
||||||
object_id: &str,
|
|
||||||
operation: ChangeOperation,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
struct Synctoken {
|
|
||||||
synctoken: i64,
|
|
||||||
}
|
|
||||||
let Synctoken { synctoken } = sqlx::query_as!(
|
|
||||||
Synctoken,
|
|
||||||
r#"
|
|
||||||
UPDATE calendars
|
|
||||||
SET synctoken = synctoken + 1
|
|
||||||
WHERE (principal, id) = (?1, ?2)
|
|
||||||
RETURNING synctoken"#,
|
|
||||||
principal,
|
|
||||||
cal_id
|
|
||||||
)
|
|
||||||
.fetch_one(&mut **tx)
|
|
||||||
.await
|
|
||||||
.map_err(crate::Error::from)?;
|
|
||||||
|
|
||||||
sqlx::query!(
|
|
||||||
r#"
|
|
||||||
INSERT INTO calendarobjectchangelog (principal, cal_id, object_id, "operation", synctoken)
|
|
||||||
VALUES (?1, ?2, ?3, ?4, (
|
|
||||||
SELECT synctoken FROM calendars WHERE (principal, id) = (?1, ?2)
|
|
||||||
))"#,
|
|
||||||
principal,
|
|
||||||
cal_id,
|
|
||||||
object_id,
|
|
||||||
operation
|
|
||||||
)
|
|
||||||
.execute(&mut **tx)
|
|
||||||
.await
|
|
||||||
.map_err(crate::Error::from)?;
|
|
||||||
Ok(format_synctoken(synctoken))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ impl SubscriptionStore for SqliteStore {
|
|||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
};
|
};
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)"#,
|
r#"REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
sub.id,
|
sub.id,
|
||||||
sub.topic,
|
sub.topic,
|
||||||
sub.expiration,
|
sub.expiration,
|
||||||
|
|||||||
15
src/app.rs
15
src/app.rs
@@ -16,7 +16,8 @@ use rustical_frontend::{FrontendConfig, frontend_router};
|
|||||||
use rustical_oidc::OidcConfig;
|
use rustical_oidc::OidcConfig;
|
||||||
use rustical_store::auth::AuthenticationProvider;
|
use rustical_store::auth::AuthenticationProvider;
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
AddressbookStore, CalendarStore, CombinedCalendarStore, ContactBirthdayStore, SubscriptionStore,
|
AddressbookStore, CalendarStore, CombinedCalendarStore, PrefixedCalendarStore,
|
||||||
|
SubscriptionStore,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -33,7 +34,11 @@ use tracing::field::display;
|
|||||||
clippy::too_many_lines,
|
clippy::too_many_lines,
|
||||||
clippy::cognitive_complexity
|
clippy::cognitive_complexity
|
||||||
)]
|
)]
|
||||||
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
pub fn make_app<
|
||||||
|
AS: AddressbookStore + PrefixedCalendarStore,
|
||||||
|
CS: CalendarStore,
|
||||||
|
S: SubscriptionStore,
|
||||||
|
>(
|
||||||
addr_store: Arc<AS>,
|
addr_store: Arc<AS>,
|
||||||
cal_store: Arc<CS>,
|
cal_store: Arc<CS>,
|
||||||
subscription_store: Arc<S>,
|
subscription_store: Arc<S>,
|
||||||
@@ -45,7 +50,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
session_cookie_samesite_strict: bool,
|
session_cookie_samesite_strict: bool,
|
||||||
payload_limit_mb: usize,
|
payload_limit_mb: usize,
|
||||||
) -> Router<()> {
|
) -> Router<()> {
|
||||||
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store.clone()));
|
let birthday_store = addr_store.clone();
|
||||||
let combined_cal_store =
|
let combined_cal_store =
|
||||||
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));
|
Arc::new(CombinedCalendarStore::new(cal_store).with_store(birthday_store));
|
||||||
|
|
||||||
@@ -173,7 +178,9 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
|||||||
tracing::debug!("unauthorized");
|
tracing::debug!("unauthorized");
|
||||||
}
|
}
|
||||||
StatusCode::NOT_FOUND => {
|
StatusCode::NOT_FOUND => {
|
||||||
tracing::warn!("client error");
|
// Clients like GNOME Calendar will try to reach /remote.php/webdav
|
||||||
|
// quite often clogging up the logs
|
||||||
|
tracing::info!("client error");
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
tracing::error!("client error");
|
tracing::error!("client error");
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@@ -13,7 +13,9 @@ use figment::Figment;
|
|||||||
use figment::providers::{Env, Format, Toml};
|
use figment::providers::{Env, Format, Toml};
|
||||||
use rustical_dav_push::DavPushController;
|
use rustical_dav_push::DavPushController;
|
||||||
use rustical_store::auth::AuthenticationProvider;
|
use rustical_store::auth::AuthenticationProvider;
|
||||||
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
use rustical_store::{
|
||||||
|
AddressbookStore, CalendarStore, CollectionOperation, PrefixedCalendarStore, SubscriptionStore,
|
||||||
|
};
|
||||||
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
|
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
|
||||||
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
||||||
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
|
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
|
||||||
@@ -29,6 +31,8 @@ mod app;
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod setup_tracing;
|
mod setup_tracing;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
@@ -56,7 +60,7 @@ async fn get_data_stores(
|
|||||||
migrate: bool,
|
migrate: bool,
|
||||||
config: &DataStoreConfig,
|
config: &DataStoreConfig,
|
||||||
) -> Result<(
|
) -> Result<(
|
||||||
Arc<impl AddressbookStore>,
|
Arc<impl AddressbookStore + PrefixedCalendarStore>,
|
||||||
Arc<impl CalendarStore>,
|
Arc<impl CalendarStore>,
|
||||||
Arc<impl SubscriptionStore>,
|
Arc<impl SubscriptionStore>,
|
||||||
Arc<impl AuthenticationProvider>,
|
Arc<impl AuthenticationProvider>,
|
||||||
@@ -69,7 +73,9 @@ async fn get_data_stores(
|
|||||||
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
let (send, recv) = tokio::sync::mpsc::channel(1000);
|
||||||
|
|
||||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||||
|
addressbook_store.repair_orphans().await?;
|
||||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||||
|
cal_store.repair_orphans().await?;
|
||||||
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||||
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
|
||||||
(
|
(
|
||||||
@@ -87,21 +93,22 @@ async fn get_data_stores(
|
|||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let parse_config = || {
|
||||||
|
Figment::new()
|
||||||
|
.merge(Toml::file(&args.config_file))
|
||||||
|
.merge(Env::prefixed("RUSTICAL_").split("__"))
|
||||||
|
.extract()
|
||||||
|
};
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?,
|
Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?,
|
||||||
Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await?,
|
Some(Command::Principals(principals_args)) => cmd_principals(principals_args).await?,
|
||||||
Some(Command::Health(health_args)) => {
|
Some(Command::Health(health_args)) => {
|
||||||
let config: Config = Figment::new()
|
let config: Config = parse_config()?;
|
||||||
.merge(Toml::file(&args.config_file))
|
|
||||||
.merge(Env::prefixed("RUSTICAL_").split("__"))
|
|
||||||
.extract()?;
|
|
||||||
cmd_health(config.http, health_args).await?;
|
cmd_health(config.http, health_args).await?;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let config: Config = Figment::new()
|
let config: Config = parse_config()?;
|
||||||
.merge(Toml::file(&args.config_file))
|
|
||||||
.merge(Env::prefixed("RUSTICAL_").split("__"))
|
|
||||||
.extract()?;
|
|
||||||
|
|
||||||
setup_tracing(&config.tracing);
|
setup_tracing(&config.tracing);
|
||||||
|
|
||||||
|
|||||||
52
src/tests.rs
Normal file
52
src/tests.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use crate::{app::make_app, config::NextcloudLoginConfig};
|
||||||
|
use rstest::rstest;
|
||||||
|
use rustical_frontend::FrontendConfig;
|
||||||
|
use rustical_store_sqlite::{
|
||||||
|
SqliteStore,
|
||||||
|
addressbook_store::SqliteAddressbookStore,
|
||||||
|
calendar_store::SqliteCalendarStore,
|
||||||
|
principal_store::SqlitePrincipalStore,
|
||||||
|
tests::{
|
||||||
|
get_test_addressbook_store, get_test_calendar_store, get_test_principal_store,
|
||||||
|
get_test_subscription_store,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
#[rstest]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_app(
|
||||||
|
#[from(get_test_calendar_store)]
|
||||||
|
#[future]
|
||||||
|
cal_store: SqliteCalendarStore,
|
||||||
|
#[from(get_test_addressbook_store)]
|
||||||
|
#[future]
|
||||||
|
addr_store: SqliteAddressbookStore,
|
||||||
|
#[from(get_test_principal_store)]
|
||||||
|
#[future]
|
||||||
|
principal_store: SqlitePrincipalStore,
|
||||||
|
#[from(get_test_subscription_store)]
|
||||||
|
#[future]
|
||||||
|
sub_store: SqliteStore,
|
||||||
|
) {
|
||||||
|
let addr_store = Arc::new(addr_store.await);
|
||||||
|
let cal_store = Arc::new(cal_store.await);
|
||||||
|
let sub_store = Arc::new(sub_store.await);
|
||||||
|
let principal_store = Arc::new(principal_store.await);
|
||||||
|
|
||||||
|
let _app = make_app(
|
||||||
|
addr_store,
|
||||||
|
cal_store,
|
||||||
|
sub_store,
|
||||||
|
principal_store,
|
||||||
|
FrontendConfig {
|
||||||
|
enabled: true,
|
||||||
|
allow_password_login: true,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
&NextcloudLoginConfig { enabled: false },
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user