Compare commits

..

43 Commits

Author SHA1 Message Date
Lennart
22a0337375 version 0.10.5 2025-11-17 19:14:17 +01:00
Lennart
21902e108a fix some error messages 2025-11-17 19:13:13 +01:00
Lennart
08f526fa5b Add startup routine to fix orphaned objects
fixes #145, related to #142
2025-11-17 19:11:30 +01:00
Lennart
ac73f3aaff addressbook_store: Commit import addressbooks to changelog 2025-11-17 18:35:10 +01:00
Lennart
9fdc8434db calendar import: log added events 2025-11-17 18:22:33 +01:00
Lennart
85f3d89235 version 0.10.4 2025-11-17 01:21:55 +01:00
Lennart
092604694a multiget: percent-decode hrefs 2025-11-17 01:21:20 +01:00
Lennart
8ef24668ba version 0.10.3 2025-11-14 11:02:27 +01:00
Lennart
416658d069 frontend: Fix missing getTimezones import in create-calendar-form
fixes #141
2025-11-14 11:01:59 +01:00
Lennart
80eae5db9e version 0.10.2 2025-11-09 21:39:09 +01:00
Lennart
66f541f1c7 Drop log level for 404 to info
fixes #139
2025-11-09 21:36:17 +01:00
Lennart
ea7196501e docs: Add verification for Google Search Console (not analytics) 2025-11-06 00:19:33 +01:00
Lennart
33d14a9ba0 sqlite_store: Add some more basic tests 2025-11-05 23:17:59 +01:00
Lennart
d843909084 Update Cargo.toml 2025-11-05 16:16:01 +01:00
Lennart
3a10a695f5 frontend: Only show logout button when logged in 2025-11-04 15:33:13 +01:00
Lennart
53c6e3b1f4 frontend: Update calendar,addressbook pages 2025-11-04 15:32:00 +01:00
Lennart
6838e8e379 frontend: update stylesheet 2025-11-04 15:31:35 +01:00
Lennart
9f28aaec41 frontend: Update deno dependencies 2025-11-04 15:31:18 +01:00
Lennart
7ec62bc6ab attempt to fix docs build 2025-11-02 22:57:29 +01:00
Lennart
9538b68e77 version 0.10.1 2025-11-02 22:21:25 +01:00
Lennart
ea5175387b update licenses 2025-11-02 22:21:16 +01:00
Lennart
0095491a20 frontend: dumb test for timezones 2025-11-02 22:17:23 +01:00
Lennart
e9392cc00b frontend: Add dropdown for timezone selection 2025-11-02 22:08:28 +01:00
Lennart
888591c952 add test case for converting filter to calendar query 2025-11-02 19:17:59 +01:00
Lennart
de77223170 Merge pull request #137 from lennart-k/feature/comp-filter
Re-implement comp-filter for calendar-query
2025-11-02 18:56:56 +01:00
Lennart
c42f8e5614 clippy appeasement 2025-11-02 18:42:55 +01:00
Lennart
f72559d027 caldav: Add supported-collation-set property 2025-11-02 18:33:54 +01:00
Lennart
167492318f xml: serialize: Support non-string text fields 2025-11-02 18:33:30 +01:00
Lennart
32f43951ac refactor text-match to support collations 2025-11-02 17:48:35 +01:00
Lennart
cd9993cd97 implement comp-filter matching for VTIMEZONE 2025-11-02 17:21:44 +01:00
Lennart
9f911fe5d7 prop-filter: Add time-range checking 2025-11-02 15:09:31 +01:00
Lennart
6361907152 re-implement comp-filter and add property filtering 2025-11-02 15:00:53 +01:00
Lennart
0c0be859f9 calendar object: Move occurence methods to CalendarObjectComponent and add get_property method 2025-11-02 15:00:13 +01:00
Lennart
d2c786eba6 merge main into feature/comp-filter 2025-11-02 13:10:56 +01:00
Lennart
dabddc6282 version 0.10.0 2025-11-01 21:49:44 +01:00
Lennart
76b4194b94 lift restriction on object_id and UID having to match
addresses #135
2025-11-01 21:48:37 +01:00
Lennart
db144ebcae calendarobject: Rename get_id to get_uid 2025-11-01 21:23:55 +01:00
Lennart
a53c333f1f version 0.9.14 2025-11-01 15:10:06 +01:00
Lennart
a05baea472 sqlite_store: Mark write transactions with BEGIN IMMEDIATE
Hopefully addresses SQLITE_BUSY error, see #131
2025-11-01 15:09:42 +01:00
Lennart
f34f7e420e Dockerfile: Update Rust to 1.91 2025-11-01 15:08:36 +01:00
Lennart
24ab323aa0 clippy appeasement 2025-11-01 14:21:44 +01:00
Lennart
f34f56ca89 update dependencies 2025-11-01 14:17:13 +01:00
Lennart
8ed4db5824 work on new comp-filter implementation 2025-10-27 18:59:00 +01:00
85 changed files with 2285 additions and 1274 deletions

View File

@@ -17,6 +17,8 @@ jobs:
with:
python-version: 3.x
- run: rustup update
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
- name: Set up build cache

View 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"
}

View File

@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "SELECT id, uid, ics FROM calendarobjects\n WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL\n AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))\n AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "uid",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 6
},
"nullable": [
false,
false,
false
]
},
"hash": "3a29efff3d3f6e1e05595d1a2d095af5fc963572c90bd10a6616af78757f8c39"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "REPLACE INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "3e1cca532372e891ab3e604ecb79311d8cd64108d4f238db4c79e9467a3b6d2e"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
"query": "SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
"describe": {
"columns": [
{
@@ -9,18 +9,24 @@
"type_info": "Text"
},
{
"name": "ics",
"name": "uid",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false,
false
]
},
"hash": "543838c030550cb09d1af08adfeade8b7ce3575d92fddbc6e9582d141bc9e49d"
"hash": "505ebe8e64ac709b230dce7150240965e45442aca6c5f3b3115738ef508939ed"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "6327bee90e5df01536a0ddb15adcc37af3027f6902aa3786365c5ab2fbf06bda"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
"query": "SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
"describe": {
"columns": [
{
@@ -9,18 +9,24 @@
"type_info": "Text"
},
{
"name": "ics",
"name": "uid",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 2,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false
]
},
"hash": "54c9c0e36a52e6963f11c6aa27f13aafb4204b8aa34b664fd825bd447db80e86"
"hash": "804ed2a4a7032e9605d1871297498f5a96de0fc816ce660c705fb28318be0d42"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "REPLACE INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "a68a1b96189b854a7ba2a3cd866ba583af5ad84bc1cd8b20cb805e9ce3bad820"
}

View 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"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects\n WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL\n AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))\n AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 6
},
"nullable": [
false,
false
]
},
"hash": "c550dbf3d5ce7069f28d767ea9045e477ef8d29d6186851760757a06dec42339"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "d498a758ed707408b00b7d2675250ea739a681ce1f009f05e97f2e101bd7e556"
}

315
Cargo.lock generated
View File

@@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
@@ -73,22 +73,22 @@ dependencies = [
[[package]]
name = "anstyle-query"
version = "1.1.4"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.10"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -139,7 +139,7 @@ dependencies = [
"rustc-hash",
"serde",
"serde_derive",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -175,7 +175,7 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -310,7 +310,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -345,9 +345,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425"
dependencies = [
"axum-core",
"bytes",
@@ -397,13 +397,14 @@ dependencies = [
[[package]]
name = "axum-extra"
version = "0.10.3"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
checksum = "dbfe9f610fe4e99cf0cfcd03ccf8c63c28c616fe714d80475ef731f3b13dd21b"
dependencies = [
"axum",
"axum-core",
"bytes",
"futures-core",
"futures-util",
"headers",
"http",
@@ -411,8 +412,6 @@ dependencies = [
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde_core",
"tower-layer",
"tower-service",
"tracing",
@@ -526,15 +525,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.10.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.43"
version = "1.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -587,9 +586,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.50"
version = "4.5.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -597,9 +596,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.50"
version = "4.5.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
dependencies = [
"anstream",
"anstyle",
@@ -616,7 +615,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -748,7 +747,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -772,7 +771,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -783,7 +782,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [
"darling_core",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -824,7 +823,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
"unicode-xid",
]
@@ -848,7 +847,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -1049,9 +1048,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "flume"
@@ -1179,7 +1178,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -1467,9 +1466,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.7.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
dependencies = [
"atomic-waker",
"bytes",
@@ -1520,9 +1519,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.17"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8"
checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -1569,7 +1568,7 @@ dependencies = [
[[package]]
name = "ical"
version = "0.11.0"
source = "git+https://github.com/lennart-k/ical-rs#38e4201d5f653b07c9800cccec93996f542267b4"
source = "git+https://github.com/lennart-k/ical-rs#474caf58acbc8ebefd90e6b848741d7ed5136d65"
dependencies = [
"chrono",
"chrono-tz",
@@ -1582,9 +1581,9 @@ dependencies = [
[[package]]
name = "icu_collections"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
dependencies = [
"displaydoc",
"potential_utf",
@@ -1595,9 +1594,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [
"displaydoc",
"litemap",
@@ -1608,11 +1607,10 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
@@ -1623,42 +1621,38 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_properties"
version = "2.0.1"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"potential_utf",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.0.1"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
[[package]]
name = "icu_provider"
version = "2.0.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [
"displaydoc",
"icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
@@ -1730,9 +1724,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
dependencies = [
"memchr",
"serde",
@@ -1838,9 +1832,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "lock_api"
@@ -1963,11 +1957,10 @@ dependencies = [
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"byteorder",
"lazy_static",
"libm",
"num-integer",
@@ -2088,9 +2081,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.74"
version = "0.10.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
dependencies = [
"bitflags",
"cfg-if",
@@ -2109,7 +2102,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -2123,9 +2116,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.110"
version = "0.9.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
dependencies = [
"cc",
"libc",
@@ -2334,7 +2327,7 @@ dependencies = [
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -2407,7 +2400,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -2476,9 +2469,9 @@ dependencies = [
[[package]]
name = "potential_utf"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
"zerovec",
]
@@ -2533,7 +2526,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
"version_check",
"yansi",
]
@@ -2558,14 +2551,14 @@ dependencies = [
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
name = "quick-xml"
version = "0.38.3"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
]
@@ -2627,9 +2620,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
@@ -2725,7 +2718,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -2856,9 +2849,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.8"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88"
dependencies = [
"const-oid",
"digest",
@@ -2899,7 +2892,7 @@ dependencies = [
"regex",
"relative-path",
"rustc_version",
"syn 2.0.108",
"syn 2.0.110",
"unicode-ident",
]
@@ -2911,7 +2904,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
dependencies = [
"quote",
"rand 0.8.5",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -2926,9 +2919,9 @@ dependencies = [
[[package]]
name = "rust-embed"
version = "8.8.0"
version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b"
checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
@@ -2937,22 +2930,22 @@ dependencies = [
[[package]]
name = "rust-embed-impl"
version = "8.8.0"
version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9"
checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.108",
"syn 2.0.110",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.8.0"
version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185"
checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
dependencies = [
"sha2",
"walkdir",
@@ -2981,7 +2974,7 @@ dependencies = [
[[package]]
name = "rustical"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"anyhow",
"argon2",
@@ -3024,7 +3017,7 @@ dependencies = [
[[package]]
name = "rustical_caldav"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"async-std",
"async-trait",
@@ -3064,7 +3057,7 @@ dependencies = [
[[package]]
name = "rustical_carddav"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"async-trait",
"axum",
@@ -3096,7 +3089,7 @@ dependencies = [
[[package]]
name = "rustical_dav"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"async-trait",
"axum",
@@ -3121,7 +3114,7 @@ dependencies = [
[[package]]
name = "rustical_dav_push"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"async-trait",
"axum",
@@ -3146,7 +3139,7 @@ dependencies = [
[[package]]
name = "rustical_frontend"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"askama",
"askama_web",
@@ -3159,6 +3152,7 @@ dependencies = [
"headers",
"hex",
"http",
"itertools 0.14.0",
"mime_guess",
"percent-encoding",
"rand 0.9.2",
@@ -3167,6 +3161,7 @@ dependencies = [
"rustical_oidc",
"rustical_store",
"serde",
"serde_json",
"thiserror 2.0.17",
"tokio",
"tower",
@@ -3175,11 +3170,12 @@ dependencies = [
"tracing",
"url",
"uuid",
"vtimezones-rs",
]
[[package]]
name = "rustical_ical"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"axum",
"chrono",
@@ -3196,7 +3192,7 @@ dependencies = [
[[package]]
name = "rustical_oidc"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"async-trait",
"axum",
@@ -3212,7 +3208,7 @@ dependencies = [
[[package]]
name = "rustical_store"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"anyhow",
"async-trait",
@@ -3245,7 +3241,7 @@ dependencies = [
[[package]]
name = "rustical_store_sqlite"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"async-trait",
"chrono",
@@ -3266,7 +3262,7 @@ dependencies = [
[[package]]
name = "rustical_xml"
version = "0.9.13"
version = "0.10.5"
dependencies = [
"quick-xml",
"thiserror 2.0.17",
@@ -3288,9 +3284,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.34"
version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [
"once_cell",
"ring",
@@ -3312,9 +3308,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.7"
version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [
"ring",
"rustls-pki-types",
@@ -3356,9 +3352,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "1.0.4"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
dependencies = [
"dyn-clone",
"ref-cast",
@@ -3429,7 +3425,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3497,9 +3493,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.15.1"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa66c845eee442168b2c8134fec70ac50dc20e760769c8ba0ad1319ca1959b04"
checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -3507,7 +3503,7 @@ dependencies = [
"indexmap 1.9.3",
"indexmap 2.12.0",
"schemars 0.9.0",
"schemars 1.0.4",
"schemars 1.1.0",
"serde_core",
"serde_json",
"serde_with_macros",
@@ -3516,14 +3512,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.15.1"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b91a903660542fced4e99881aa481bdbaec1634568ee02e0b8bd57c64cb38955"
checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3691,7 +3687,7 @@ dependencies = [
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3714,7 +3710,7 @@ dependencies = [
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.108",
"syn 2.0.110",
"tokio",
"url",
]
@@ -3866,7 +3862,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3888,9 +3884,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.108"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -3914,7 +3910,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3943,7 +3939,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3954,7 +3950,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -3999,9 +3995,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [
"displaydoc",
"zerovec",
@@ -4048,7 +4044,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -4074,9 +4070,9 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.16"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5"
checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
dependencies = [
"bytes",
"futures-core",
@@ -4360,7 +4356,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -4456,24 +4452,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.20"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-normalization"
version = "0.1.24"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.3"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-xid"
@@ -4643,7 +4639,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
"wasm-bindgen-shared",
]
@@ -4678,9 +4674,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8"
checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
dependencies = [
"rustls-pki-types",
]
@@ -4725,7 +4721,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -4736,7 +4732,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -5011,20 +5007,20 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "xml_derive"
version = "0.1.0"
version = "0.10.5"
dependencies = [
"darling",
"heck",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -5035,11 +5031,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "yoke"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
@@ -5047,13 +5042,13 @@ dependencies = [
[[package]]
name = "yoke-derive"
version = "0.8.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
"synstructure",
]
@@ -5074,7 +5069,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]
[[package]]
@@ -5094,7 +5089,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
"synstructure",
]
@@ -5106,9 +5101,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]]
name = "zerotrie"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [
"displaydoc",
"yoke",
@@ -5117,9 +5112,9 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.4"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [
"yoke",
"zerofrom",
@@ -5128,11 +5123,11 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
"syn 2.0.110",
]

View File

@@ -2,7 +2,8 @@
members = ["crates/*"]
[workspace.package]
version = "0.9.13"
version = "0.10.5"
rust-version = "1.91"
edition = "2024"
description = "A CalDAV server"
documentation = "https://lennart-k.github.io/rustical/"
@@ -12,6 +13,7 @@ license = "AGPL-3.0-or-later"
[package]
name = "rustical"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -35,6 +37,17 @@ opentelemetry = [
debug = 0
[workspace.dependencies]
rustical_dav = { path = "./crates/dav/" }
rustical_dav_push = { path = "./crates/dav_push/" }
rustical_store = { path = "./crates/store/" }
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
rustical_caldav = { path = "./crates/caldav/" }
rustical_carddav = { path = "./crates/carddav/" }
rustical_frontend = { path = "./crates/frontend/" }
rustical_xml = { path = "./crates/xml/" }
rustical_oidc = { path = "./crates/oidc/" }
rustical_ical = { path = "./crates/ical/" }
matchit = "0.9"
uuid = { version = "1.11", features = ["v4", "fast-rng"] }
async-trait = "0.1"
@@ -108,20 +121,10 @@ tower-http = { version = "0.6", features = [
"catch-panic",
] }
percent-encoding = "2.3"
rustical_dav = { path = "./crates/dav/" }
rustical_dav_push = { path = "./crates/dav_push/" }
rustical_store = { path = "./crates/store/" }
rustical_store_sqlite = { path = "./crates/store_sqlite/" }
rustical_caldav = { path = "./crates/caldav/" }
rustical_carddav = { path = "./crates/carddav/" }
rustical_frontend = { path = "./crates/frontend/" }
rustical_xml = { path = "./crates/xml/" }
rustical_oidc = { path = "./crates/oidc/" }
rustical_ical = { path = "./crates/ical/" }
chrono-tz = "0.10"
chrono-humanize = "0.2"
rand = "0.9"
axum-extra = { version = "0.10", features = ["typed-header"] }
axum-extra = { version = "0.12", features = ["typed-header"] }
rrule = "0.14"
argon2 = "0.5"
rpassword = "7.3"
@@ -147,19 +150,19 @@ openssl = { version = "0.10", features = ["vendored"] }
async-std = { version = "1.13", features = ["attributes"] }
[dependencies]
rustical_store = { workspace = true }
rustical_store_sqlite = { workspace = true }
rustical_caldav = { workspace = true }
rustical_store.workspace = true
rustical_store_sqlite.workspace = true
rustical_caldav.workspace = true
rustical_carddav.workspace = true
rustical_frontend = { workspace = true }
toml = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
anyhow = { workspace = true }
rustical_frontend.workspace = true
toml.workspace = true
serde.workspace = true
tokio.workspace = true
tracing.workspace = true
anyhow.workspace = true
clap.workspace = true
sqlx = { workspace = true }
async-trait = { workspace = true }
sqlx.workspace = true
async-trait.workspace = true
uuid.workspace = true
axum.workspace = true

View File

@@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM rust:1.90-alpine AS chef
FROM --platform=$BUILDPLATFORM rust:1.91-alpine AS chef
ARG TARGETPLATFORM
ARG BUILDPLATFORM

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_caldav"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -17,21 +18,21 @@ serde_json.workspace = true
axum.workspace = true
axum-extra.workspace = true
tower.workspace = true
async-trait = { workspace = true }
thiserror = { workspace = true }
quick-xml = { workspace = true }
tracing = { workspace = true }
futures-util = { workspace = true }
derive_more = { workspace = true }
base64 = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }
rustical_dav = { workspace = true }
rustical_store = { workspace = true }
chrono = { workspace = true }
chrono-tz = { workspace = true }
sha2 = { workspace = true }
async-trait.workspace = true
thiserror.workspace = true
quick-xml.workspace = true
tracing.workspace = true
futures-util.workspace = true
derive_more.workspace = true
base64.workspace = true
serde.workspace = true
tokio.workspace = true
url.workspace = true
rustical_dav.workspace = true
rustical_store.workspace = true
chrono.workspace = true
chrono-tz.workspace = true
sha2.workspace = true
ical.workspace = true
percent-encoding.workspace = true
rustical_xml.workspace = true

View File

@@ -82,7 +82,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
let objects = expanded_cals
.into_iter()
.map(|cal| cal.generate())
.map(CalendarObject::from_ics)
.map(|ics| CalendarObject::from_ics(ics, None))
.collect::<Result<Vec<_>, _>>()?;
let new_cal = Calendar {
principal,

View File

@@ -26,16 +26,18 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
let mut not_found = vec![];
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('/');
if let Some(object_id) = filename.strip_suffix(".ics") {
match store.get_object(principal, cal_id, object_id, false).await {
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()),
}
} else {
not_found.push(href.to_owned());
not_found.push(href.to_string());
}
} else {
not_found.push(href.to_owned());

View File

@@ -0,0 +1,345 @@
use crate::calendar::methods::report::calendar_query::{
TimeRangeElement,
prop_filter::{PropFilterElement, PropFilterable},
};
use ical::parser::ical::component::IcalTimeZone;
use rustical_ical::{CalendarObject, CalendarObjectComponent, CalendarObjectType};
use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)]
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
pub struct CompFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) is_not_defined: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) time_range: Option<TimeRangeElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) prop_filter: Vec<PropFilterElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) comp_filter: Vec<CompFilterElement>,
#[xml(ty = "attr")]
pub(crate) name: String,
}
pub trait CompFilterable: PropFilterable + Sized {
fn get_comp_name(&self) -> &'static str;
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool;
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool;
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
// The scope of the
// CALDAV:comp-filter XML element is the calendar object when used as
// a child of the CALDAV:filter XML element. The scope of the
// CALDAV:comp-filter XML element is the enclosing calendar component
// when used as a child of another CALDAV:comp-filter XML element
fn matches(&self, comp_filter: &CompFilterElement) -> bool {
let name_matches = self.get_comp_name() == comp_filter.name;
match (comp_filter.is_not_defined.is_some(), name_matches) {
// We are the component that's not supposed to be defined
(true, true)
// We don't match
| (false, false) => return false,
// We shall not be and indeed we aren't
(true, false) => return true,
_ => {}
}
if let Some(time_range) = comp_filter.time_range.as_ref()
&& !self.match_time_range(time_range)
{
return false;
}
for prop_filter in &comp_filter.prop_filter {
if !prop_filter.match_component(self) {
return false;
}
}
comp_filter
.comp_filter
.iter()
.all(|filter| self.match_subcomponents(filter))
}
}
impl CompFilterable for CalendarObject {
fn get_comp_name(&self) -> &'static str {
"VCALENDAR"
}
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
// VCALENDAR has no concept of time range
false
}
fn match_subcomponents(&self, comp_filter: &CompFilterElement) -> bool {
let mut matches = self
.get_vtimezones()
.values()
.map(|tz| tz.matches(comp_filter))
.chain([self.get_data().matches(comp_filter)]);
if comp_filter.is_not_defined.is_some() {
matches.all(|x| x)
} else {
matches.any(|x| x)
}
}
}
impl CompFilterable for IcalTimeZone {
fn get_comp_name(&self) -> &'static str {
"VTIMEZONE"
}
fn match_time_range(&self, _time_range: &TimeRangeElement) -> bool {
false
}
fn match_subcomponents(&self, _comp_filter: &CompFilterElement) -> bool {
true
}
}
impl CompFilterable for CalendarObjectComponent {
fn get_comp_name(&self) -> &'static str {
CalendarObjectType::from(self).as_str()
}
fn match_time_range(&self, time_range: &TimeRangeElement) -> bool {
if let Some(start) = &time_range.start
&& let Some(last_occurence) = self.get_last_occurence().unwrap_or(None)
&& **start > last_occurence.utc()
{
return false;
}
if let Some(end) = &time_range.end
&& let Some(first_occurence) = self.get_first_occurence().unwrap_or(None)
&& **end < first_occurence.utc()
{
return false;
}
true
}
fn match_subcomponents(&self, _comp_filter: &CompFilterElement) -> bool {
// TODO: Properly check subcomponents
true
}
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use rustical_ical::{CalendarObject, UtcDateTime};
use crate::calendar::methods::report::calendar_query::{
CompFilterable, TextMatchElement, TimeRangeElement,
comp_filter::CompFilterElement,
prop_filter::PropFilterElement,
text_match::{NegateCondition, TextCollation},
};
const ICS: &str = r"BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
END:VTIMEZONE
BEGIN:VEVENT
UID:318ec6503573d9576818daf93dac07317058d95c
DTSTAMP:20250502T132758Z
DTSTART;TZID=Europe/Berlin:20250506T090000
DTEND;TZID=Europe/Berlin:20250506T092500
SEQUENCE:2
SUMMARY:weekly stuff
TRANSP:OPAQUE
RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
END:VEVENT
END:VCALENDAR";
#[test]
fn test_comp_filter_matching() {
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let comp_filter = CompFilterElement {
is_not_defined: Some(()),
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![],
comp_filter: vec![],
};
assert!(!object.matches(&comp_filter), "filter: wants no VCALENDAR");
let comp_filter = CompFilterElement {
is_not_defined: None,
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
name: "VTODO".to_string(),
is_not_defined: None,
time_range: None,
prop_filter: vec![],
comp_filter: vec![],
}],
};
assert!(!object.matches(&comp_filter), "filter matches VTODO");
let comp_filter = CompFilterElement {
is_not_defined: None,
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
name: "VEVENT".to_string(),
is_not_defined: None,
time_range: None,
prop_filter: vec![],
comp_filter: vec![],
}],
};
assert!(object.matches(&comp_filter), "filter matches VEVENT");
let comp_filter = CompFilterElement {
is_not_defined: None,
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![
PropFilterElement {
is_not_defined: None,
name: "VERSION".to_string(),
time_range: None,
text_match: Some(TextMatchElement {
needle: "2.0".to_string(),
collation: TextCollation::default(),
negate_condition: NegateCondition::default(),
}),
param_filter: vec![],
},
PropFilterElement {
is_not_defined: Some(()),
name: "STUFF".to_string(),
time_range: None,
text_match: None,
param_filter: vec![],
},
],
comp_filter: vec![CompFilterElement {
name: "VEVENT".to_string(),
is_not_defined: None,
time_range: None,
prop_filter: vec![PropFilterElement {
is_not_defined: None,
name: "SUMMARY".to_string(),
time_range: None,
text_match: Some(TextMatchElement {
collation: TextCollation::default(),
negate_condition: NegateCondition(false),
needle: "weekly".to_string(),
}),
param_filter: vec![],
}],
comp_filter: vec![],
}],
};
assert!(
object.matches(&comp_filter),
"Some prop filters on VCALENDAR and VEVENT"
);
}
#[test]
fn test_comp_filter_time_range() {
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let comp_filter = CompFilterElement {
is_not_defined: None,
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
name: "VEVENT".to_string(),
is_not_defined: None,
time_range: Some(TimeRangeElement {
start: Some(UtcDateTime(
Utc.with_ymd_and_hms(2025, 4, 1, 0, 0, 0).unwrap(),
)),
end: Some(UtcDateTime(
Utc.with_ymd_and_hms(2025, 8, 1, 0, 0, 0).unwrap(),
)),
}),
prop_filter: vec![],
comp_filter: vec![],
}],
};
assert!(
object.matches(&comp_filter),
"event should lie in time range"
);
let comp_filter = CompFilterElement {
is_not_defined: None,
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
name: "VEVENT".to_string(),
is_not_defined: None,
time_range: Some(TimeRangeElement {
start: Some(UtcDateTime(
Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(),
)),
end: Some(UtcDateTime(
Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(),
)),
}),
prop_filter: vec![],
comp_filter: vec![],
}],
};
assert!(
!object.matches(&comp_filter),
"event should not lie in time range"
);
}
#[test]
fn test_match_timezone() {
let object = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let comp_filter = CompFilterElement {
is_not_defined: None,
name: "VCALENDAR".to_string(),
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
name: "VTIMEZONE".to_string(),
is_not_defined: None,
time_range: None,
prop_filter: vec![PropFilterElement {
is_not_defined: None,
name: "TZID".to_string(),
time_range: None,
text_match: Some(TextMatchElement {
collation: TextCollation::AsciiCasemap,
negate_condition: NegateCondition::default(),
needle: "Europe/Berlin".to_string(),
}),
param_filter: vec![],
}],
comp_filter: vec![],
}],
};
assert!(
object.matches(&comp_filter),
"Timezone should be Europe/Berlin"
);
}
}

View File

@@ -1,4 +1,10 @@
use crate::calendar_object::CalendarObjectPropWrapperName;
use crate::{
calendar::methods::report::calendar_query::{
TextMatchElement,
comp_filter::{CompFilterElement, CompFilterable},
},
calendar_object::CalendarObjectPropWrapperName,
};
use rustical_dav::xml::PropfindType;
use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_store::calendar_store::CalendarQuery;
@@ -26,112 +32,6 @@ pub struct ParamFilterElement {
pub(crate) name: String,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub struct TextMatchElement {
#[xml(ty = "attr")]
pub(crate) collation: String,
#[xml(ty = "attr")]
// "yes" or "no", default: "no"
pub(crate) negate_condition: Option<String>,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
pub struct PropFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) is_not_defined: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) time_range: Option<TimeRangeElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) text_match: Option<TextMatchElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) param_filter: Vec<ParamFilterElement>,
#[xml(ty = "attr")]
pub(crate) name: String,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)]
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
pub struct CompFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) is_not_defined: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) time_range: Option<TimeRangeElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) prop_filter: Vec<PropFilterElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) comp_filter: Vec<CompFilterElement>,
#[xml(ty = "attr")]
pub(crate) name: String,
}
impl CompFilterElement {
// match the VCALENDAR part
pub fn matches_root(&self, cal_object: &CalendarObject) -> bool {
let comp_vcal = self.name == "VCALENDAR";
match (self.is_not_defined, comp_vcal) {
// Client wants VCALENDAR to not exist but we are a VCALENDAR
(Some(()), true) |
// Client is asking for something different than a vcalendar
(None, false) => return false,
_ => {}
}
if self.time_range.is_some() {
// <time-range> should be applied on VEVENT/VTODO but not on VCALENDAR
return false;
}
// TODO: Implement prop-filter at some point
// Apply sub-comp-filters on VEVENT/VTODO/VJOURNAL component
if self
.comp_filter
.iter()
.all(|filter| filter.matches(cal_object))
{
return true;
}
false
}
// match the VEVENT/VTODO/VJOURNAL part
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
let comp_name_matches = self.name == cal_object.get_component_name();
match (self.is_not_defined, comp_name_matches) {
// Client wants VCALENDAR to not exist but we are a VCALENDAR
(Some(()), true) |
// Client is asking for something different than a vcalendar
(None, false) => return false,
_ => {}
}
// TODO: Implement prop-filter (and comp-filter?) at some point
if let Some(time_range) = &self.time_range {
if let Some(start) = &time_range.start
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
&& **start > last_occurence.utc()
{
return false;
}
if let Some(end) = &time_range.end
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
&& **end < first_occurence.utc()
{
return false;
}
}
true
}
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)]
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
@@ -142,8 +42,9 @@ pub struct FilterElement {
}
impl FilterElement {
#[must_use]
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
self.comp_filter.matches_root(cal_object)
cal_object.matches(&self.comp_filter)
}
}
@@ -187,3 +88,45 @@ impl From<&CalendarQueryRequest> for CalendarQuery {
value.filter.as_ref().map(Self::from).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use crate::calendar::methods::report::calendar_query::{
CompFilterElement, FilterElement, TimeRangeElement,
};
use chrono::{NaiveDate, TimeZone, Utc};
use rustical_ical::UtcDateTime;
use rustical_store::calendar_store::CalendarQuery;
#[test]
fn test_filter_element_calendar_query() {
let filter = FilterElement {
comp_filter: CompFilterElement {
name: "VCALENDAR".to_string(),
is_not_defined: None,
time_range: None,
prop_filter: vec![],
comp_filter: vec![CompFilterElement {
name: "VEVENT".to_string(),
is_not_defined: None,
time_range: Some(TimeRangeElement {
start: Some(UtcDateTime(
Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(),
)),
end: Some(UtcDateTime(
Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(),
)),
}),
prop_filter: vec![],
comp_filter: vec![],
}],
},
};
let derived_query: CalendarQuery = (&filter).into();
let query = CalendarQuery {
time_start: Some(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()),
time_end: Some(NaiveDate::from_ymd_opt(2024, 8, 1).unwrap()),
};
assert_eq!(derived_query, query);
}
}

View File

@@ -2,8 +2,17 @@ use crate::Error;
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
mod comp_filter;
mod elements;
mod prop_filter;
pub mod text_match;
#[allow(unused_imports)]
pub use comp_filter::{CompFilterElement, CompFilterable};
pub use elements::*;
#[allow(unused_imports)]
pub use prop_filter::{PropFilterElement, PropFilterable};
#[allow(unused_imports)]
pub use text_match::TextMatchElement;
pub async fn get_objects_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest,
@@ -29,8 +38,10 @@ mod tests {
calendar::methods::report::{
ReportRequest,
calendar_query::{
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
PropFilterElement, TextMatchElement,
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
comp_filter::CompFilterElement,
prop_filter::PropFilterElement,
text_match::{NegateCondition, TextCollation},
},
},
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
@@ -90,16 +101,18 @@ mod tests {
prop_filter: vec![PropFilterElement {
name: "ATTENDEE".to_owned(),
text_match: Some(TextMatchElement {
collation: "i;ascii-casemap".to_owned(),
negate_condition: None
collation: TextCollation::AsciiCasemap,
negate_condition: NegateCondition(false),
needle: "mailto:lisa@example.com".to_string()
}),
is_not_defined: None,
param_filter: vec![ParamFilterElement {
is_not_defined: None,
name: "PARTSTAT".to_owned(),
text_match: Some(TextMatchElement {
collation: "i;ascii-casemap".to_owned(),
negate_condition: None
collation: TextCollation::AsciiCasemap,
negate_condition: NegateCondition(false),
needle: "NEEDS-ACTION".to_string()
}),
}],
time_range: None

View File

@@ -0,0 +1,127 @@
use std::collections::HashMap;
use ical::{
generator::{IcalCalendar, IcalEvent},
parser::{
Component,
ical::component::{IcalJournal, IcalTimeZone, IcalTodo},
},
property::Property,
};
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectComponent, UtcDateTime};
use rustical_xml::XmlDeserialize;
use crate::calendar::methods::report::calendar_query::{
ParamFilterElement, TextMatchElement, TimeRangeElement,
};
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
pub struct PropFilterElement {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) is_not_defined: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) time_range: Option<TimeRangeElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) text_match: Option<TextMatchElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
pub(crate) param_filter: Vec<ParamFilterElement>,
#[xml(ty = "attr")]
pub(crate) name: String,
}
impl PropFilterElement {
pub fn match_component(&self, comp: &impl PropFilterable) -> bool {
let property = comp.get_property(&self.name);
let property = match (self.is_not_defined.is_some(), property) {
// We are the component that's not supposed to be defined
(true, Some(_))
// We don't match
| (false, None) => return false,
// We shall not be and indeed we aren't
(true, None) => return true,
(false, Some(property)) => property
};
if let Some(TimeRangeElement { start, end }) = &self.time_range {
// TODO: Respect timezones
let Ok(timestamp) = CalDateTime::parse_prop(property, &HashMap::default()) else {
return false;
};
let timestamp = timestamp.utc();
if let Some(UtcDateTime(start)) = start
&& start > &timestamp
{
return false;
}
if let Some(UtcDateTime(end)) = end
&& end < &timestamp
{
return false;
}
return true;
}
if let Some(text_match) = &self.text_match
&& !text_match.match_property(property)
{
return false;
}
// TODO: param-filter
true
}
}
pub trait PropFilterable {
fn get_property(&self, name: &str) -> Option<&Property>;
}
impl PropFilterable for CalendarObject {
fn get_property(&self, name: &str) -> Option<&Property> {
Self::get_property(self, name)
}
}
impl PropFilterable for IcalEvent {
fn get_property(&self, name: &str) -> Option<&Property> {
Component::get_property(self, name)
}
}
impl PropFilterable for IcalTodo {
fn get_property(&self, name: &str) -> Option<&Property> {
Component::get_property(self, name)
}
}
impl PropFilterable for IcalJournal {
fn get_property(&self, name: &str) -> Option<&Property> {
Component::get_property(self, name)
}
}
impl PropFilterable for IcalCalendar {
fn get_property(&self, name: &str) -> Option<&Property> {
Component::get_property(self, name)
}
}
impl PropFilterable for IcalTimeZone {
fn get_property(&self, name: &str) -> Option<&Property> {
Component::get_property(self, name)
}
}
impl PropFilterable for CalendarObjectComponent {
fn get_property(&self, name: &str) -> Option<&Property> {
match self {
Self::Event(event, _) => PropFilterable::get_property(&event.event, name),
Self::Todo(todo, _) => PropFilterable::get_property(todo, name),
Self::Journal(journal, _) => PropFilterable::get_property(journal, name),
}
}
}

View File

@@ -0,0 +1,103 @@
use ical::property::Property;
use rustical_xml::{ValueDeserialize, XmlDeserialize};
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum TextCollation {
#[default]
AsciiCasemap,
Octet,
}
impl TextCollation {
// Check whether a haystack contains a needle respecting the collation
#[must_use]
pub fn match_text(&self, needle: &str, haystack: &str) -> bool {
match self {
// https://datatracker.ietf.org/doc/html/rfc4790#section-9.2
Self::AsciiCasemap => haystack
.to_ascii_uppercase()
.contains(&needle.to_ascii_uppercase()),
Self::Octet => haystack.contains(needle),
}
}
}
impl AsRef<str> for TextCollation {
fn as_ref(&self) -> &str {
match self {
Self::AsciiCasemap => "i;ascii-casemap",
Self::Octet => "i;octet",
}
}
}
impl ValueDeserialize for TextCollation {
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
match val {
"i;ascii-casemap" => Ok(Self::AsciiCasemap),
"i;octet" => Ok(Self::Octet),
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
"Invalid collation: {val}"
))),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct NegateCondition(pub bool);
impl ValueDeserialize for NegateCondition {
fn deserialize(val: &str) -> Result<Self, rustical_xml::XmlError> {
match val {
"yes" => Ok(Self(true)),
"no" => Ok(Self(false)),
_ => Err(rustical_xml::XmlError::InvalidVariant(format!(
"Invalid negate-condition parameter: {val}"
))),
}
}
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub struct TextMatchElement {
#[xml(ty = "attr", default = "Default::default")]
pub collation: TextCollation,
#[xml(ty = "attr", default = "Default::default")]
pub(crate) negate_condition: NegateCondition,
#[xml(ty = "text")]
pub(crate) needle: String,
}
impl TextMatchElement {
#[must_use]
pub fn match_property(&self, property: &Property) -> bool {
let Self {
collation,
negate_condition,
needle,
} = self;
let matches = property
.value
.as_ref()
.is_some_and(|haystack| collation.match_text(needle, haystack));
// XOR
negate_condition.0 ^ matches
}
}
#[cfg(test)]
mod tests {
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
#[test]
fn test_collation() {
assert!(TextCollation::AsciiCasemap.match_text("GrüN", "grün"));
assert!(!TextCollation::AsciiCasemap.match_text("GrÜN", "grün"));
assert!(!TextCollation::Octet.match_text("GrÜN", "grün"));
assert!(TextCollation::Octet.match_text("hallo", "hallo"));
assert!(TextCollation::AsciiCasemap.match_text("HaLlo", "hAllo"));
}
}

View File

@@ -27,7 +27,7 @@ use sync_collection::handle_sync_collection;
use tracing::instrument;
mod calendar_multiget;
mod calendar_query;
pub mod calendar_query;
mod sync_collection;
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]

View File

@@ -3,6 +3,8 @@ use rustical_ical::CalendarObjectType;
use rustical_xml::{XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray;
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
pub struct SupportedCalendarComponent {
#[xml(ty = "attr")]
@@ -36,6 +38,28 @@ impl From<SupportedCalendarComponentSet> for Vec<CalendarObjectType> {
}
}
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
pub struct SupportedCollation(#[xml(ty = "text")] pub TextCollation);
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq)]
pub struct SupportedCollationSet(
#[xml(
ns = "rustical_dav::namespace::NS_CALDAV",
flatten,
rename = "supported-collation"
)]
pub Vec<SupportedCollation>,
);
impl Default for SupportedCollationSet {
fn default() -> Self {
Self(vec![
SupportedCollation(TextCollation::AsciiCasemap),
SupportedCollation(TextCollation::Octet),
])
}
}
#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)]
pub struct CalendarData {
#[xml(ty = "attr")]

View File

@@ -1,6 +1,6 @@
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
use crate::Error;
use crate::calendar::prop::ReportMethod;
use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
use chrono::{DateTime, Utc};
use derive_more::derive::{From, Into};
use ical::IcalParser;
@@ -39,6 +39,8 @@ pub enum CalendarProp {
SupportedCalendarComponentSet(SupportedCalendarComponentSet),
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
SupportedCalendarData(SupportedCalendarData),
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
SupportedCollationSet(SupportedCollationSet),
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
MaxResourceSize(i64),
#[xml(skip_deserializing)]
@@ -156,6 +158,9 @@ impl Resource for CalendarResource {
CalendarPropName::SupportedCalendarData => {
CalendarProp::SupportedCalendarData(SupportedCalendarData::default())
}
CalendarPropName::SupportedCollationSet => {
CalendarProp::SupportedCollationSet(SupportedCollationSet::default())
}
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000),
CalendarPropName::SupportedReportSet => {
CalendarProp::SupportedReportSet(SupportedReportSet::all())
@@ -244,6 +249,7 @@ impl Resource for CalendarResource {
}
CalendarProp::TimezoneServiceSet(_)
| CalendarProp::SupportedCalendarData(_)
| CalendarProp::SupportedCollationSet(_)
| CalendarProp::MaxResourceSize(_)
| CalendarProp::SupportedReportSet(_)
| CalendarProp::Source(_)
@@ -283,6 +289,7 @@ impl Resource for CalendarResource {
}
CalendarPropName::TimezoneServiceSet
| CalendarPropName::SupportedCalendarData
| CalendarPropName::SupportedCollationSet
| CalendarPropName::MaxResourceSize
| CalendarPropName::SupportedReportSet
| CalendarPropName::Source

View File

@@ -11,6 +11,7 @@
<calendar-order xmlns="http://apple.com/ns/ical/"/>
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav"/>
<max-resource-size xmlns="DAV:"/>
<supported-report-set xmlns="DAV:"/>
<source xmlns="http://calendarserver.org/ns/"/>
@@ -160,6 +161,10 @@ END:VCALENDAR
<CAL:supported-calendar-data>
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
</CAL:supported-calendar-data>
<CAL:supported-collation-set>
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
<CAL:supported-collation>i;octet</CAL:supported-collation>
</CAL:supported-collation-set>
<max-resource-size>10000000</max-resource-size>
<supported-report-set>
<supported-report>

View File

@@ -11,7 +11,7 @@ use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
use rustical_store::auth::Principal;
use std::str::FromStr;
use tracing::{debug, error, instrument};
use tracing::{debug, instrument};
#[instrument(skip(cal_store))]
pub async fn get_event<C: CalendarStore>(
@@ -78,18 +78,10 @@ pub async fn put_event<C: CalendarStore>(
true
};
let Ok(object) = CalendarObject::from_ics(body.clone()) else {
let Ok(object) = CalendarObject::from_ics(body.clone(), Some(object_id)) else {
debug!("invalid calendar data:\n{body}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
};
if object.get_id() != object_id {
error!(
"Calendar object UID and file name not matching: UID={}, filename={}",
object.get_id(),
object_id
);
return Err(Error::PreconditionFailed(Precondition::MatchingUid));
}
cal_store
.put_object(principal, calendar_id, object, overwrite)
.await?;

View File

@@ -12,8 +12,6 @@ pub enum Precondition {
#[error("valid-calendar-data")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
ValidCalendarData,
#[error("matching-uid")]
MatchingUid,
}
impl IntoResponse for Precondition {

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_carddav"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -11,19 +12,19 @@ publish = false
axum.workspace = true
axum-extra.workspace = true
tower.workspace = true
async-trait = { workspace = true }
thiserror = { workspace = true }
quick-xml = { workspace = true }
tracing = { workspace = true }
futures-util = { workspace = true }
derive_more = { workspace = true }
base64 = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
url = { workspace = true }
rustical_dav = { workspace = true }
rustical_store = { workspace = true }
chrono = { workspace = true }
async-trait.workspace = true
thiserror.workspace = true
quick-xml.workspace = true
tracing.workspace = true
futures-util.workspace = true
derive_more.workspace = true
base64.workspace = true
serde.workspace = true
tokio.workspace = true
url.workspace = true
rustical_dav.workspace = true
rustical_store.workspace = true
chrono.workspace = true
rustical_xml.workspace = true
uuid.workspace = true
rustical_dav_push.workspace = true

View File

@@ -34,7 +34,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
let mut not_found = vec![];
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('/');
if let Some(object_id) = filename.strip_suffix(".vcf") {
match store
@@ -42,11 +44,11 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
.await
{
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()),
}
} else {
not_found.push(href.to_owned());
not_found.push(href.to_string());
}
} else {
not_found.push(href.to_owned());

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_dav"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -11,7 +12,6 @@ publish = false
axum.workspace = true
tower.workspace = true
axum-extra.workspace = true
rustical_xml.workspace = true
async-trait.workspace = true
futures-util.workspace = true

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_dav_push"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -9,15 +10,15 @@ publish = false
[dependencies]
rustical_xml.workspace = true
async-trait = { workspace = true }
futures-util = { workspace = true }
quick-xml = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
itertools = { workspace = true }
log = { workspace = true }
derive_more = { workspace = true }
tracing = { workspace = true }
async-trait.workspace = true
futures-util.workspace = true
quick-xml.workspace = true
serde.workspace = true
thiserror.workspace = true
itertools.workspace = true
log.workspace = true
derive_more.workspace = true
tracing.workspace = true
reqwest.workspace = true
tokio.workspace = true
rustical_dav.workspace = true

View File

@@ -59,7 +59,7 @@ impl<S: SubscriptionStore> DavPushController<S> {
let mut latest_messages = HashMap::new();
for message in messages {
if matches!(message.data, CollectionOperationInfo::Content { .. }) {
latest_messages.insert(message.topic.to_string(), message);
latest_messages.insert(message.topic.clone(), message);
}
}
let messages = latest_messages.into_values();
@@ -156,12 +156,13 @@ impl<S: SubscriptionStore> DavPushController<S> {
) -> Result<(), NotifierError> {
if subsciption.public_key_type != "p256dh" {
return Err(NotifierError::InvalidPublicKeyType(
subsciption.public_key_type.to_string(),
subsciption.public_key_type.clone(),
));
}
let endpoint = subsciption.push_resource.parse().map_err(|_| {
NotifierError::InvalidEndpointUrl(subsciption.push_resource.to_string())
})?;
let endpoint = subsciption
.push_resource
.parse()
.map_err(|_| NotifierError::InvalidEndpointUrl(subsciption.push_resource.clone()))?;
let ua_public = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&subsciption.public_key)
.map_err(|_| NotifierError::InvalidKeyEncoding)?;

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_frontend"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -39,3 +40,6 @@ headers.workspace = true
tower-sessions.workspace = true
percent-encoding.workspace = true
tower-http = { workspace = true, optional = true }
vtimezones-rs.workspace = true
serde_json.workspace = true
itertools.workspace = true

View File

@@ -13,6 +13,6 @@
"imports": {
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.5",
"lit": "npm:lit@^3.3.1",
"vite": "npm:vite@^7.1.7"
"vite": "npm:vite@^7.1.12"
}
}

View File

@@ -1,145 +1,145 @@
{
"version": "5",
"specifiers": {
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.7__picomatch@4.0.3",
"npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.12__picomatch@4.0.3",
"npm:lit@^3.3.1": "3.3.1",
"npm:vite@*": "7.1.7_picomatch@4.0.3",
"npm:vite@^7.1.7": "7.1.7_picomatch@4.0.3"
"npm:vite@*": "7.1.12_picomatch@4.0.3",
"npm:vite@^7.1.12": "7.1.12_picomatch@4.0.3"
},
"npm": {
"@deno/vite-plugin@1.0.5_vite@7.1.7__picomatch@4.0.3": {
"@deno/vite-plugin@1.0.5_vite@7.1.12__picomatch@4.0.3": {
"integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==",
"dependencies": [
"vite"
]
},
"@esbuild/aix-ppc64@0.25.10": {
"integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"@esbuild/aix-ppc64@0.25.12": {
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"os": ["aix"],
"cpu": ["ppc64"]
},
"@esbuild/android-arm64@0.25.10": {
"integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"@esbuild/android-arm64@0.25.12": {
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"os": ["android"],
"cpu": ["arm64"]
},
"@esbuild/android-arm@0.25.10": {
"integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"@esbuild/android-arm@0.25.12": {
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"os": ["android"],
"cpu": ["arm"]
},
"@esbuild/android-x64@0.25.10": {
"integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"@esbuild/android-x64@0.25.12": {
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"os": ["android"],
"cpu": ["x64"]
},
"@esbuild/darwin-arm64@0.25.10": {
"integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"@esbuild/darwin-arm64@0.25.12": {
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"os": ["darwin"],
"cpu": ["arm64"]
},
"@esbuild/darwin-x64@0.25.10": {
"integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"@esbuild/darwin-x64@0.25.12": {
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@esbuild/freebsd-arm64@0.25.10": {
"integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"@esbuild/freebsd-arm64@0.25.12": {
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"os": ["freebsd"],
"cpu": ["arm64"]
},
"@esbuild/freebsd-x64@0.25.10": {
"integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"@esbuild/freebsd-x64@0.25.12": {
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@esbuild/linux-arm64@0.25.10": {
"integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"@esbuild/linux-arm64@0.25.12": {
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@esbuild/linux-arm@0.25.10": {
"integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"@esbuild/linux-arm@0.25.12": {
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"os": ["linux"],
"cpu": ["arm"]
},
"@esbuild/linux-ia32@0.25.10": {
"integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"@esbuild/linux-ia32@0.25.12": {
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"os": ["linux"],
"cpu": ["ia32"]
},
"@esbuild/linux-loong64@0.25.10": {
"integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"@esbuild/linux-loong64@0.25.12": {
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"os": ["linux"],
"cpu": ["loong64"]
},
"@esbuild/linux-mips64el@0.25.10": {
"integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"@esbuild/linux-mips64el@0.25.12": {
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"os": ["linux"],
"cpu": ["mips64el"]
},
"@esbuild/linux-ppc64@0.25.10": {
"integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"@esbuild/linux-ppc64@0.25.12": {
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"os": ["linux"],
"cpu": ["ppc64"]
},
"@esbuild/linux-riscv64@0.25.10": {
"integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"@esbuild/linux-riscv64@0.25.12": {
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@esbuild/linux-s390x@0.25.10": {
"integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"@esbuild/linux-s390x@0.25.12": {
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"os": ["linux"],
"cpu": ["s390x"]
},
"@esbuild/linux-x64@0.25.10": {
"integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
"@esbuild/linux-x64@0.25.12": {
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"os": ["linux"],
"cpu": ["x64"]
},
"@esbuild/netbsd-arm64@0.25.10": {
"integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"@esbuild/netbsd-arm64@0.25.12": {
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"os": ["netbsd"],
"cpu": ["arm64"]
},
"@esbuild/netbsd-x64@0.25.10": {
"integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"@esbuild/netbsd-x64@0.25.12": {
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"os": ["netbsd"],
"cpu": ["x64"]
},
"@esbuild/openbsd-arm64@0.25.10": {
"integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
"@esbuild/openbsd-arm64@0.25.12": {
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"os": ["openbsd"],
"cpu": ["arm64"]
},
"@esbuild/openbsd-x64@0.25.10": {
"integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"@esbuild/openbsd-x64@0.25.12": {
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"os": ["openbsd"],
"cpu": ["x64"]
},
"@esbuild/openharmony-arm64@0.25.10": {
"integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
"@esbuild/openharmony-arm64@0.25.12": {
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"os": ["openharmony"],
"cpu": ["arm64"]
},
"@esbuild/sunos-x64@0.25.10": {
"integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
"@esbuild/sunos-x64@0.25.12": {
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"os": ["sunos"],
"cpu": ["x64"]
},
"@esbuild/win32-arm64@0.25.10": {
"integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"@esbuild/win32-arm64@0.25.12": {
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@esbuild/win32-ia32@0.25.10": {
"integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"@esbuild/win32-ia32@0.25.12": {
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@esbuild/win32-x64@0.25.10": {
"integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"@esbuild/win32-x64@0.25.12": {
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"os": ["win32"],
"cpu": ["x64"]
},
@@ -152,113 +152,113 @@
"@lit-labs/ssr-dom-shim"
]
},
"@rollup/rollup-android-arm-eabi@4.52.2": {
"integrity": "sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==",
"@rollup/rollup-android-arm-eabi@4.52.5": {
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"os": ["android"],
"cpu": ["arm"]
},
"@rollup/rollup-android-arm64@4.52.2": {
"integrity": "sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==",
"@rollup/rollup-android-arm64@4.52.5": {
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"os": ["android"],
"cpu": ["arm64"]
},
"@rollup/rollup-darwin-arm64@4.52.2": {
"integrity": "sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==",
"@rollup/rollup-darwin-arm64@4.52.5": {
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"os": ["darwin"],
"cpu": ["arm64"]
},
"@rollup/rollup-darwin-x64@4.52.2": {
"integrity": "sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==",
"@rollup/rollup-darwin-x64@4.52.5": {
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@rollup/rollup-freebsd-arm64@4.52.2": {
"integrity": "sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==",
"@rollup/rollup-freebsd-arm64@4.52.5": {
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"os": ["freebsd"],
"cpu": ["arm64"]
},
"@rollup/rollup-freebsd-x64@4.52.2": {
"integrity": "sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==",
"@rollup/rollup-freebsd-x64@4.52.5": {
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@rollup/rollup-linux-arm-gnueabihf@4.52.2": {
"integrity": "sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==",
"@rollup/rollup-linux-arm-gnueabihf@4.52.5": {
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"os": ["linux"],
"cpu": ["arm"]
},
"@rollup/rollup-linux-arm-musleabihf@4.52.2": {
"integrity": "sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==",
"@rollup/rollup-linux-arm-musleabihf@4.52.5": {
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"os": ["linux"],
"cpu": ["arm"]
},
"@rollup/rollup-linux-arm64-gnu@4.52.2": {
"integrity": "sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==",
"@rollup/rollup-linux-arm64-gnu@4.52.5": {
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@rollup/rollup-linux-arm64-musl@4.52.2": {
"integrity": "sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==",
"@rollup/rollup-linux-arm64-musl@4.52.5": {
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@rollup/rollup-linux-loong64-gnu@4.52.2": {
"integrity": "sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==",
"@rollup/rollup-linux-loong64-gnu@4.52.5": {
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"os": ["linux"],
"cpu": ["loong64"]
},
"@rollup/rollup-linux-ppc64-gnu@4.52.2": {
"integrity": "sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==",
"@rollup/rollup-linux-ppc64-gnu@4.52.5": {
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"os": ["linux"],
"cpu": ["ppc64"]
},
"@rollup/rollup-linux-riscv64-gnu@4.52.2": {
"integrity": "sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==",
"@rollup/rollup-linux-riscv64-gnu@4.52.5": {
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@rollup/rollup-linux-riscv64-musl@4.52.2": {
"integrity": "sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==",
"@rollup/rollup-linux-riscv64-musl@4.52.5": {
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@rollup/rollup-linux-s390x-gnu@4.52.2": {
"integrity": "sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==",
"@rollup/rollup-linux-s390x-gnu@4.52.5": {
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"os": ["linux"],
"cpu": ["s390x"]
},
"@rollup/rollup-linux-x64-gnu@4.52.2": {
"integrity": "sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==",
"@rollup/rollup-linux-x64-gnu@4.52.5": {
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"os": ["linux"],
"cpu": ["x64"]
},
"@rollup/rollup-linux-x64-musl@4.52.2": {
"integrity": "sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==",
"@rollup/rollup-linux-x64-musl@4.52.5": {
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"os": ["linux"],
"cpu": ["x64"]
},
"@rollup/rollup-openharmony-arm64@4.52.2": {
"integrity": "sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==",
"@rollup/rollup-openharmony-arm64@4.52.5": {
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"os": ["openharmony"],
"cpu": ["arm64"]
},
"@rollup/rollup-win32-arm64-msvc@4.52.2": {
"integrity": "sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==",
"@rollup/rollup-win32-arm64-msvc@4.52.5": {
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@rollup/rollup-win32-ia32-msvc@4.52.2": {
"integrity": "sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==",
"@rollup/rollup-win32-ia32-msvc@4.52.5": {
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@rollup/rollup-win32-x64-gnu@4.52.2": {
"integrity": "sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==",
"@rollup/rollup-win32-x64-gnu@4.52.5": {
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"os": ["win32"],
"cpu": ["x64"]
},
"@rollup/rollup-win32-x64-msvc@4.52.2": {
"integrity": "sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==",
"@rollup/rollup-win32-x64-msvc@4.52.5": {
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"os": ["win32"],
"cpu": ["x64"]
},
@@ -268,8 +268,8 @@
"@types/trusted-types@2.0.7": {
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
},
"esbuild@0.25.10": {
"integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"esbuild@0.25.12": {
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"optionalDependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
@@ -355,8 +355,8 @@
"source-map-js"
]
},
"rollup@4.52.2": {
"integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==",
"rollup@4.52.5": {
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"dependencies": [
"@types/estree"
],
@@ -397,8 +397,8 @@
"picomatch"
]
},
"vite@7.1.7_picomatch@4.0.3": {
"integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==",
"vite@7.1.12_picomatch@4.0.3": {
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dependencies": [
"esbuild",
"fdir",
@@ -417,7 +417,7 @@
"dependencies": [
"npm:@deno/vite-plugin@^1.0.5",
"npm:lit@^3.3.1",
"npm:vite@^7.1.7"
"npm:vite@^7.1.12"
]
}
}

View File

@@ -2,11 +2,17 @@ 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-calendar-form")
export class CreateCalendarForm extends LitElement {
constructor() {
super()
this.fetchTimezones()
}
async fetchTimezones() {
this.timezones = await getTimezones()
}
protected override createRenderRoot() {
@@ -36,6 +42,8 @@ export class CreateCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() {
return html`
@@ -65,7 +73,12 @@ export class CreateCalendarForm extends LitElement {
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
<option value="">No timezone</option>
${this.timezones.map(timezone => html`
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
`)}
</select>
</label>
<br>
<label>

View File

@@ -2,11 +2,17 @@ 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("edit-calendar-form")
export class EditCalendarForm extends LitElement {
constructor() {
super()
this.fetchTimezones()
}
async fetchTimezones() {
this.timezones = await getTimezones()
}
protected override createRenderRoot() {
@@ -36,7 +42,8 @@ export class EditCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() {
return html`
@@ -51,7 +58,12 @@ export class EditCalendarForm extends LitElement {
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
<option value="">No timezone</option>
${this.timezones.map(timezone => html`
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
`)}
</select>
</label>
<br>
<label>

View File

@@ -0,0 +1,12 @@
let timezonesPromise = null
export 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
}

View File

@@ -2,6 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
@@ -27,6 +28,11 @@ let CreateCalendarForm = class extends i {
this.components = /* @__PURE__ */ new Set();
this.dialog = e();
this.form = e();
this.timezones = [];
this.fetchTimezones();
}
async fetchTimezones() {
this.timezones = await getTimezones();
}
createRenderRoot() {
return this;
@@ -59,7 +65,12 @@ let CreateCalendarForm = class extends i {
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
<option value="">No timezone</option>
${this.timezones.map((timezone) => x`
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
`)}
</select>
</label>
<br>
<label>
@@ -179,6 +190,9 @@ __decorateClass([
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "components", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezones", 2);
CreateCalendarForm = __decorateClass([
t("create-calendar-form")
], CreateCalendarForm);

View File

@@ -2,6 +2,7 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
import { g as getTimezones } from "./timezones-B0vBBzCP.mjs";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => {
@@ -22,6 +23,11 @@ let EditCalendarForm = class extends i {
this.components = /* @__PURE__ */ new Set();
this.dialog = e();
this.form = e();
this.timezones = [];
this.fetchTimezones();
}
async fetchTimezones() {
this.timezones = await getTimezones();
}
createRenderRoot() {
return this;
@@ -39,7 +45,12 @@ let EditCalendarForm = class extends i {
<br>
<label>
Timezone (optional)
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
<option value="">No timezone</option>
${this.timezones.map((timezone) => x`
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
`)}
</select>
</label>
<br>
<label>
@@ -150,6 +161,9 @@ __decorateClass([
}
})
], EditCalendarForm.prototype, "components", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "timezones", 2);
EditCalendarForm = __decorateClass([
t("edit-calendar-form")
], EditCalendarForm);

View 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

View File

@@ -79,9 +79,6 @@ header {
nav {
display: flex;
border-radius: 12px;
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 5%);
a {
text-decoration: none;
margin: 4px 8px;
@@ -259,19 +256,6 @@ ul.collection-list {
margin: 8px initial;
}
.comps {
display: inline;
span {
margin: 0 2px;
background: var(--primary-color);
color: var(--text-on-primary-color);
font-size: .8em;
padding: 3px 8px;
border-radius: 12px;
}
}
.description {
grid-area: description;
white-space: nowrap;
@@ -363,3 +347,16 @@ svg.icon {
color: var(--text-on-background-color);
stroke: var(--text-on-background-color);
}
.component-chips {
display: inline;
span.chip {
margin: 0 2px;
background: var(--primary-color);
color: var(--text-on-primary-color);
font-size: .8em;
padding: 3px 8px;
border-radius: 12px;
}
}

View File

@@ -8,9 +8,9 @@
<span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps">
<div class="component-chips">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
<span class="chip">{{ comp }}</span>
{% endfor %}
</div>
</span>
@@ -58,9 +58,9 @@
<span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps">
<div class="component-chips">
{% for comp in calendar.components %}
<span>{{ comp }}</span>
<span class="chip">{{ comp }}</span>
{% endfor %}
</div>
</span>
@@ -85,4 +85,3 @@
{% endif %}
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
<import-calendar-form user="{{ user.id }}"></import-calendar-form>

View File

@@ -14,9 +14,11 @@
<header>
<a class="logo" href="/frontend/user">RustiCal</a>
{% block header_center %}{% endblock %}
{% if self.get_user().is_some() %}
<form method="POST" action="/frontend/logout" class="logout_form">
<button type="submit">Log out</button>
</form>
{% endif %}
</header>
{% endblock %}
<div id="app">

View File

@@ -8,6 +8,7 @@
<h1>{{ name }}</h1>
{% if let Some(description) = addressbook.description %}<p>{{ description }}</p>{% endif%}
<pre>{{ addressbook|json }}</pre>
<h2>Debug information</h2>
<pre>{{ addressbook|json(2) }}</pre>
{% endblock %}

View File

@@ -5,7 +5,15 @@
{% block content %}
{% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
<h1>{{ calendar.principal }}/{{ name }}</h1>
<h1>
{{ calendar.principal }}/{{ name }}
<div class="component-chips">
{% for comp in calendar.components %}
<span class="chip">{{ comp }}</span>
{% endfor %}
</div>
</h1>
{% if let Some(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(subscription_url) = calendar.subscription_url %}
@@ -13,19 +21,7 @@
<a href="{{ subscription_url }}">{{ subscription_url }}</a>
{% endif %}
<h2>Components</h2>
<ul>
{% for comp in calendar.components %}
<li>{{ comp.as_str() }}</li>
{% endfor %}
</ul>
<h2>Timezone</h2>
{% if let Some(timezone_id) = calendar.timezone_id %}
<p>{{ timezone_id }}</p>
{% endif %}
<pre>{{ calendar|json }}</pre>
<h2>Debug information</h2>
<pre>{{ calendar|json(2) }}</pre>
{%endblock %}

View File

@@ -33,6 +33,7 @@ use crate::routes::{
app_token::{route_delete_app_token, route_post_app_token},
calendar::{route_calendar, route_calendar_restore},
login::{route_get_login, route_post_login, route_post_logout},
timezones::route_timezones,
user::{route_get_home, route_root, route_user_named},
};
#[cfg(not(feature = "dev"))]
@@ -79,7 +80,11 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
.route("/", get(route_root))
.nest("/user", user_router)
.route("/login", get(route_get_login).post(route_post_login::<AP>))
.route("/logout", post(route_post_logout));
.route("/logout", post(route_post_logout))
.route(
"/_timezones.json",
get(route_timezones).head(route_timezones),
);
#[cfg(not(feature = "dev"))]
let mut router = router.route_service("/assets/{*file}", EmbedService::<Assets>::default());

View File

@@ -2,7 +2,7 @@ use super::{
NextcloudFlow, NextcloudFlows, NextcloudLoginPoll, NextcloudLoginResponse,
NextcloudSuccessResponse,
};
use crate::routes::app_token::generate_app_token;
use crate::{pages::DefaultLayoutData, routes::app_token::generate_app_token};
use askama::Template;
use axum::{
Extension, Form, Json,
@@ -100,6 +100,12 @@ struct NextcloudLoginPage {
app_name: String,
}
impl DefaultLayoutData for NextcloudLoginPage {
fn get_user(&self) -> Option<&Principal> {
None
}
}
#[instrument(skip(state))]
pub async fn get_nextcloud_flow(
Extension(state): Extension<Arc<NextcloudFlows>>,
@@ -130,6 +136,13 @@ pub struct NextcloudAuthorizeForm {
#[template(path = "pages/nextcloud_login/success.html")]
struct NextcloudLoginSuccessPage {
app_name: String,
user: Principal,
}
impl DefaultLayoutData for NextcloudLoginSuccessPage {
fn get_user(&self) -> Option<&Principal> {
Some(&self.user)
}
}
#[instrument(skip(state))]
@@ -150,6 +163,7 @@ pub async fn post_nextcloud_flow(
Ok(Html(
NextcloudLoginSuccessPage {
app_name: flow.app_name.clone(),
user,
}
.render()
.unwrap(),

View File

@@ -1 +1,8 @@
use rustical_store::auth::Principal;
pub mod user;
/// Required by the base layout
pub trait DefaultLayoutData {
fn get_user(&self) -> Option<&Principal>;
}

View File

@@ -2,6 +2,8 @@ use askama::Template;
use askama_web::WebTemplate;
use rustical_store::auth::Principal;
use crate::pages::DefaultLayoutData;
pub trait Section: Template {
fn name() -> &'static str;
}
@@ -12,3 +14,9 @@ pub struct UserPage<S: Section> {
pub user: Principal,
pub section: S,
}
impl<S: Section> DefaultLayoutData for UserPage<S> {
fn get_user(&self) -> Option<&Principal> {
Some(&self.user)
}
}

View File

@@ -12,10 +12,19 @@ use headers::Referer;
use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
use crate::pages::DefaultLayoutData;
#[derive(Template, WebTemplate)]
#[template(path = "pages/addressbook.html")]
struct AddressbookPage {
addressbook: Addressbook,
user: Principal,
}
impl DefaultLayoutData for AddressbookPage {
fn get_user(&self) -> Option<&Principal> {
Some(&self.user)
}
}
pub async fn route_addressbook<AS: AddressbookStore>(
@@ -28,6 +37,7 @@ pub async fn route_addressbook<AS: AddressbookStore>(
}
Ok(AddressbookPage {
addressbook: store.get_addressbook(&owner, &addrbook_id, true).await?,
user,
}
.into_response())
}

View File

@@ -1,5 +1,4 @@
use std::sync::Arc;
use crate::pages::DefaultLayoutData;
use askama::Template;
use askama_web::WebTemplate;
use axum::{
@@ -11,11 +10,19 @@ use axum_extra::TypedHeader;
use headers::Referer;
use http::StatusCode;
use rustical_store::{Calendar, CalendarStore, auth::Principal};
use std::sync::Arc;
#[derive(Template, WebTemplate)]
#[template(path = "pages/calendar.html")]
struct CalendarPage {
calendar: Calendar,
user: Principal,
}
impl DefaultLayoutData for CalendarPage {
fn get_user(&self) -> Option<&Principal> {
Some(&self.user)
}
}
pub async fn route_calendar<C: CalendarStore>(
@@ -28,6 +35,7 @@ pub async fn route_calendar<C: CalendarStore>(
}
Ok(CalendarPage {
calendar: store.get_calendar(&owner, &cal_id, true).await?,
user,
}
.into_response())
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use crate::{FrontendConfig, OidcConfig};
use crate::{FrontendConfig, OidcConfig, pages::DefaultLayoutData};
use askama::Template;
use askama_web::WebTemplate;
use axum::{
@@ -24,6 +24,12 @@ struct LoginPage<'a> {
allow_password_login: bool,
}
impl DefaultLayoutData for LoginPage<'_> {
fn get_user(&self) -> Option<&rustical_store::auth::Principal> {
None
}
}
struct OidcProviderData<'a> {
pub name: &'a str,
pub redirect_url: String,

View File

@@ -4,4 +4,5 @@ pub mod app_token;
pub mod calendar;
pub mod calendars;
pub mod login;
pub mod timezones;
pub mod user;

View File

@@ -0,0 +1,39 @@
use headers::{CacheControl, ContentType, HeaderMapExt};
use http::{HeaderMap, HeaderValue, Method};
use itertools::Itertools;
use std::{sync::LazyLock, time::Duration};
static VTIMEZONES_JSON: LazyLock<String> = LazyLock::new(|| {
serde_json::to_string(
&vtimezones_rs::VTIMEZONES
.keys()
.sorted()
.collect::<Vec<_>>(),
)
.unwrap()
});
pub async fn route_timezones(method: Method) -> (HeaderMap, &'static str) {
let mut headers = HeaderMap::new();
headers.typed_insert(ContentType::json());
headers.insert(
"ETag",
HeaderValue::from_static(vtimezones_rs::IANA_TZDB_VERSION),
);
headers.typed_insert(CacheControl::new().with_max_age(Duration::from_hours(2)));
if method == Method::HEAD {
return (headers, "");
}
(headers, VTIMEZONES_JSON.as_str())
}
#[cfg(test)]
#[tokio::test]
async fn test_vtimezones_json() -> () {
// Since there's an unwrap make sure this doesn't fail
assert!(!VTIMEZONES_JSON.as_str().is_empty());
assert!(route_timezones(Method::HEAD).await.1.is_empty());
assert!(!route_timezones(Method::GET).await.1.is_empty());
}

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_ical"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -97,7 +97,8 @@ impl AddressObject {
let uid = format!("{}-anniversary", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(format!(
Some(CalendarObject::from_ics(
format!(
r"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
@@ -116,7 +117,9 @@ DESCRIPTION:💍 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR",
))?)
),
None,
)?)
} else {
None
},
@@ -136,7 +139,8 @@ END:VCALENDAR",
let uid = format!("{}-birthday", self.get_id());
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
Some(CalendarObject::from_ics(format!(
Some(CalendarObject::from_ics(
format!(
r"BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
@@ -155,7 +159,9 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
END:VALARM
END:VEVENT
END:VCALENDAR",
))?)
),
None,
)?)
} else {
None
},

View File

@@ -2,7 +2,7 @@ use axum::{http::StatusCode, response::IntoResponse};
use crate::CalDateTimeError;
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum Error {
#[error("Invalid ics/vcf input: {0}")]
InvalidData(String),

View File

@@ -251,7 +251,7 @@ END:VEVENT\r\n",
#[test]
fn test_expand_recurrence() {
let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
let event = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
panic!()
};

View File

@@ -64,6 +64,19 @@ pub enum CalendarObjectComponent {
Journal(IcalJournal, Vec<IcalJournal>),
}
impl CalendarObjectComponent {
#[must_use]
pub fn get_uid(&self) -> &str {
match &self {
// We've made sure before that the first component exists and all components share the
// same UID
Self::Todo(todo, _) => todo.get_uid(),
Self::Event(event, _) => event.event.get_uid(),
Self::Journal(journal, _) => journal.get_uid(),
}
}
}
impl From<&CalendarObjectComponent> for CalendarObjectType {
fn from(value: &CalendarObjectComponent) -> Self {
match value {
@@ -135,18 +148,47 @@ impl CalendarObjectComponent {
}
Ok(Self::Journal(main_journal, overrides))
}
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self {
Self::Event(main_event, overrides) => Ok(overrides
.iter()
.chain(std::iter::once(main_event))
.map(super::event::EventObject::get_dtstart)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.min()),
_ => Ok(None),
}
}
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self {
Self::Event(main_event, overrides) => Ok(overrides
.iter()
.chain(std::iter::once(main_event))
.map(super::event::EventObject::get_last_occurence)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.max()),
_ => Ok(None),
}
}
}
#[derive(Debug, Clone)]
pub struct CalendarObject {
data: CalendarObjectComponent,
properties: Vec<Property>,
id: String,
ics: String,
vtimezones: HashMap<String, IcalTimeZone>,
}
impl CalendarObject {
pub fn from_ics(ics: String) -> Result<Self, Error> {
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() {
@@ -202,6 +244,7 @@ impl CalendarObject {
};
Ok(Self {
id: id.unwrap_or_else(|| data.get_uid().to_owned()),
data,
properties: cal.properties,
ics,
@@ -220,20 +263,19 @@ impl CalendarObject {
}
#[must_use]
pub fn get_id(&self) -> &str {
match &self.data {
// We've made sure before that the first component exists and all components share the
// same UID
CalendarObjectComponent::Todo(todo, _) => todo.get_uid(),
CalendarObjectComponent::Event(event, _) => event.event.get_uid(),
CalendarObjectComponent::Journal(journal, _) => journal.get_uid(),
pub fn get_uid(&self) -> &str {
self.data.get_uid()
}
#[must_use]
pub fn get_id(&self) -> &str {
&self.id
}
#[must_use]
pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(self.get_id());
hasher.update(self.get_uid());
hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize())
}
@@ -254,31 +296,11 @@ impl CalendarObject {
}
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self.data {
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
.iter()
.chain(std::iter::once(main_event))
.map(super::event::EventObject::get_dtstart)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.min()),
_ => Ok(None),
}
self.data.get_first_occurence()
}
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self.data {
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
.iter()
.chain(std::iter::once(main_event))
.map(super::event::EventObject::get_last_occurence)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.max()),
_ => Ok(None),
}
self.data.get_last_occurence()
}
pub fn expand_recurrence(
@@ -299,4 +321,11 @@ impl CalendarObject {
_ => Ok(self.get_ics().to_string()),
}
}
#[must_use]
pub fn get_property(&self, name: &str) -> Option<&Property> {
self.properties
.iter()
.find(|property| property.name == name)
}
}

View File

@@ -13,7 +13,7 @@ const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
pub const LOCAL_DATE: &str = "%Y%m%d";
#[derive(Debug, thiserror::Error)]
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CalDateTimeError {
#[error(
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid"

View File

@@ -25,6 +25,6 @@ END:VCALENDAR
#[test]
fn parse_calendar_object() {
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string()).unwrap();
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string(), None).unwrap();
object.expand_recurrence(None, None).unwrap();
}

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_oidc"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -14,7 +15,7 @@ reqwest.workspace = true
thiserror.workspace = true
async-trait.workspace = true
axum.workspace = true
tower-sessions = "0.14"
tower-sessions.workspace = true
axum-extra.workspace = true
headers.workspace = true
tracing.workspace = true

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_store"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -8,16 +9,16 @@ license.workspace = true
publish = false
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
ical = { workspace = true }
chrono = { workspace = true }
regex = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
chrono-tz = { workspace = true }
anyhow.workspace = true
async-trait.workspace = true
serde.workspace = true
sha2.workspace = true
ical.workspace = true
chrono.workspace = true
regex.workspace = true
thiserror.workspace = true
tracing.workspace = true
chrono-tz.workspace = true
derive_more = { workspace = true, features = ["as_ref"] }
rustical_xml.workspace = true
tokio.workspace = true
@@ -34,7 +35,7 @@ tower-sessions.workspace = true
vtimezones-rs.workspace = true
[dev-dependencies]
rstest = { workspace = true }
rstest_reuse = { workspace = true }
rstest.workspace = true
rstest_reuse.workspace = true
rustical_store_sqlite.workspace = true
tokio.workspace = true

View File

@@ -2,7 +2,7 @@ use crate::synctoken::format_synctoken;
use chrono::NaiveDateTime;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Addressbook {
pub id: String,
pub principal: String,

View File

@@ -1,11 +1,10 @@
use std::str::FromStr;
use crate::synctoken::format_synctoken;
use chrono::NaiveDateTime;
use rustical_ical::CalendarObjectType;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CalendarMetadata {
// Attributes that may be outsourced
pub displayname: Option<String>,
@@ -14,7 +13,7 @@ pub struct CalendarMetadata {
pub color: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Calendar {
// Attributes that may be outsourced
#[serde(flatten)]

View File

@@ -3,7 +3,7 @@ use async_trait::async_trait;
use chrono::NaiveDate;
use rustical_ical::CalendarObject;
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct CalendarQuery {
pub time_start: Option<NaiveDate>,
pub time_end: Option<NaiveDate>,

View File

@@ -41,6 +41,11 @@ impl Error {
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[must_use]
pub const fn is_not_found(&self) -> bool {
matches!(self, Self::NotFound)
}
}
impl IntoResponse for Error {

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_store_sqlite"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true
@@ -15,12 +16,12 @@ rstest.workspace = true
[dependencies]
tokio.workspace = true
rustical_store = { workspace = true }
async-trait = { workspace = true }
serde = { workspace = true }
sqlx = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
rustical_store.workspace = true
async-trait.workspace = true
serde.workspace = true
sqlx.workspace = true
thiserror.workspace = true
tracing.workspace = true
derive_more.workspace = true
chrono.workspace = true
password-auth.workspace = true

View File

@@ -0,0 +1,47 @@
DROP INDEX idx_calobjs_uid;
ALTER TABLE calendarobjects RENAME TO calendarobjects_old;
CREATE TABLE calendarobjects (
principal TEXT NOT NULL,
cal_id TEXT NOT NULL,
id TEXT NOT NULL, -- filename
ics TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
-- For more efficient calendar-queries
first_occurence DATE,
last_occurence DATE,
etag TEXT,
object_type INTEGER NOT NULL, -- VEVENT(0)/VTODO(1)/VJOURNAL(2)
CONSTRAINT pk_calendarobject_id PRIMARY KEY (principal, cal_id, id),
CONSTRAINT fk_calendarobject_calendar FOREIGN KEY (principal, cal_id)
REFERENCES calendars (principal, id) ON DELETE CASCADE
);
INSERT INTO calendarobjects (
principal,
cal_id,
id,
ics,
updated_at,
deleted_at,
first_occurence,
last_occurence,
etag,
object_type
) SELECT
principal,
cal_id,
id,
ics,
updated_at,
deleted_at,
first_occurence,
last_occurence,
etag,
object_type
FROM calendarobjects_old;
DROP TABLE calendarobjects_old;

View File

@@ -0,0 +1,53 @@
-- Adds the column "uid" and populates it with data from "id"
ALTER TABLE calendarobjects RENAME TO calendarobjects_old;
CREATE TABLE calendarobjects (
principal TEXT NOT NULL,
cal_id TEXT NOT NULL,
id TEXT NOT NULL, -- filename
"uid" TEXT NOT NULL, -- global identifier
ics TEXT NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
deleted_at DATETIME,
-- For more efficient calendar-queries
first_occurence DATE,
last_occurence DATE,
etag TEXT,
object_type INTEGER NOT NULL, -- VEVENT(0)/VTODO(1)/VJOURNAL(2)
CONSTRAINT pk_calendarobject_id PRIMARY KEY (principal, cal_id, id),
CONSTRAINT uq_calendarobject_uid UNIQUE (principal, cal_id, "uid"),
CONSTRAINT fk_calendarobject_calendar FOREIGN KEY (principal, cal_id)
REFERENCES calendars (principal, id) ON DELETE CASCADE
);
CREATE INDEX idx_calobjs_uid ON calendarobjects (principal, cal_id, "uid");
INSERT INTO calendarobjects (
principal,
cal_id,
id,
"uid",
ics,
updated_at,
deleted_at,
first_occurence,
last_occurence,
etag,
object_type
) SELECT
principal,
cal_id,
id,
id AS "uid",
ics,
updated_at,
deleted_at,
first_occurence,
last_occurence,
etag,
object_type
FROM calendarobjects_old;
DROP TABLE calendarobjects_old;

View File

@@ -1,4 +1,5 @@
use super::ChangeOperation;
use crate::BEGIN_IMMEDIATE;
use async_trait::async_trait;
use derive_more::derive::Constructor;
use rustical_ical::AddressObject;
@@ -8,7 +9,7 @@ use rustical_store::{
};
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
use tracing::{error, instrument};
use tracing::{error, instrument, warn};
#[derive(Debug, Clone)]
struct AddressObjectRow {
@@ -31,6 +32,60 @@ pub struct 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
);
log_object_operation(
&mut tx,
&row.principal,
&row.addressbook_id,
&row.id,
operation,
)
.await?;
}
tx.commit().await.map_err(crate::Error::from)?;
Ok(())
}
async fn _get_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: &str,
@@ -89,9 +144,9 @@ impl SqliteAddressbookStore {
async fn _update_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: String,
id: String,
addressbook: Addressbook,
principal: &str,
id: &str,
addressbook: &Addressbook,
) -> Result<(), rustical_store::Error> {
let result = sqlx::query!(
r#"UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?
@@ -115,7 +170,7 @@ impl SqliteAddressbookStore {
async fn _insert_addressbook<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
addressbook: Addressbook,
addressbook: &Addressbook,
) -> Result<(), rustical_store::Error> {
sqlx::query!(
r#"INSERT INTO addressbooks (principal, id, displayname, description, push_topic)
@@ -282,9 +337,9 @@ impl SqliteAddressbookStore {
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: String,
addressbook_id: String,
object: AddressObject,
principal: &str,
addressbook_id: &str,
object: &AddressObject,
overwrite: bool,
) -> Result<(), rustical_store::Error> {
let (object_id, vcf) = (object.get_id(), object.get_vcf());
@@ -396,7 +451,7 @@ impl AddressbookStore for SqliteAddressbookStore {
id: String,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
Self::_update_addressbook(&self.db, principal, id, addressbook).await
Self::_update_addressbook(&self.db, &principal, &id, &addressbook).await
}
#[instrument]
@@ -404,7 +459,7 @@ impl AddressbookStore for SqliteAddressbookStore {
&self,
addressbook: Addressbook,
) -> Result<(), rustical_store::Error> {
Self::_insert_addressbook(&self.db, addressbook).await
Self::_insert_addressbook(&self.db, &addressbook).await
}
#[instrument]
@@ -414,7 +469,11 @@ impl AddressbookStore for SqliteAddressbookStore {
addressbook_id: &str,
use_trashbin: bool,
) -> Result<(), rustical_store::Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let addressbook =
match Self::_get_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await {
@@ -508,18 +567,15 @@ impl AddressbookStore for SqliteAddressbookStore {
object: AddressObject,
overwrite: bool,
) -> Result<(), rustical_store::Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let object_id = object.get_id().to_owned();
Self::_put_object(
&mut *tx,
principal.clone(),
addressbook_id.clone(),
object,
overwrite,
)
.await?;
Self::_put_object(&mut *tx, &principal, &addressbook_id, &object, overwrite).await?;
let sync_token = log_object_operation(
&mut tx,
@@ -554,7 +610,11 @@ impl AddressbookStore for SqliteAddressbookStore {
object_id: &str,
use_trashbin: bool,
) -> Result<(), rustical_store::Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
@@ -589,7 +649,11 @@ impl AddressbookStore for SqliteAddressbookStore {
addressbook_id: &str,
object_id: &str,
) -> Result<(), rustical_store::Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
@@ -611,7 +675,7 @@ impl AddressbookStore for SqliteAddressbookStore {
.await?
.push_topic,
}) {
error!("Push notification about deleted addressbook failed: {err}");
error!("Push notification about restored addressbook object failed: {err}");
}
Ok(())
@@ -624,7 +688,11 @@ impl AddressbookStore for SqliteAddressbookStore {
objects: Vec<AddressObject>,
merge_existing: bool,
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let existing =
match Self::_get_addressbook(&mut *tx, &addressbook.principal, &addressbook.id, true)
@@ -638,21 +706,44 @@ impl AddressbookStore for SqliteAddressbookStore {
return Err(Error::AlreadyExists);
}
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 {
Self::_put_object(
&mut *tx,
addressbook.principal.clone(),
addressbook.id.clone(),
object,
&addressbook.principal,
&addressbook.id,
&object,
false,
)
.await?;
sync_token = Some(
log_object_operation(
&mut tx,
&addressbook.principal,
&addressbook.id,
object.get_id(),
ChangeOperation::Add,
)
.await?,
);
}
tx.commit().await.map_err(crate::Error::from)?;
if let Some(sync_token) = sync_token
&& let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self
.get_addressbook(&addressbook.principal, &addressbook.id, true)
.await?
.push_topic,
})
{
error!("Push notification about imported addressbook failed: {err}");
}
Ok(())
}
}
@@ -664,7 +755,7 @@ async fn log_object_operation(
addressbook_id: &str,
object_id: &str,
operation: ChangeOperation,
) -> Result<String, sqlx::Error> {
) -> Result<String, Error> {
struct Synctoken {
synctoken: i64,
}
@@ -679,7 +770,8 @@ async fn log_object_operation(
addressbook_id
)
.fetch_one(&mut **tx)
.await?;
.await
.map_err(crate::Error::from)?;
sqlx::query!(
r#"
@@ -693,6 +785,7 @@ async fn log_object_operation(
operation
)
.execute(&mut **tx)
.await?;
.await
.map_err(crate::Error::from)?;
Ok(format_synctoken(synctoken))
}

View File

@@ -1,4 +1,5 @@
use super::ChangeOperation;
use crate::BEGIN_IMMEDIATE;
use async_trait::async_trait;
use chrono::TimeDelta;
use derive_more::derive::Constructor;
@@ -10,25 +11,26 @@ use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
use tokio::sync::mpsc::Sender;
use tracing::{error, instrument};
use tracing::{error, instrument, warn};
#[derive(Debug, Clone)]
struct CalendarObjectRow {
id: String,
ics: String,
uid: String,
}
impl TryFrom<CalendarObjectRow> for CalendarObject {
type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
let object = Self::from_ics(value.ics)?;
if object.get_id() != value.id {
let object = Self::from_ics(value.ics, Some(value.id))?;
if object.get_uid() != value.uid {
return Err(rustical_store::Error::IcalError(
rustical_ical::Error::InvalidData(format!(
"object_id={} and UID={} don't match",
object.get_id(),
value.id
"uid={} and UID={} don't match",
value.uid,
object.get_uid()
)),
));
}
@@ -92,6 +94,53 @@ pub struct SqliteCalendarStore {
}
impl SqliteCalendarStore {
// 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
);
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>>(
executor: E,
principal: &str,
@@ -280,7 +329,7 @@ impl SqliteCalendarStore {
) -> Result<Vec<CalendarObject>, Error> {
sqlx::query_as!(
CalendarObjectRow,
"SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
principal,
cal_id
)
@@ -305,7 +354,7 @@ impl SqliteCalendarStore {
sqlx::query_as!(
CalendarObjectRow,
r"SELECT id, ics FROM calendarobjects
r"SELECT id, uid, ics FROM calendarobjects
WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL
AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))
AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))
@@ -334,7 +383,7 @@ impl SqliteCalendarStore {
) -> Result<CalendarObject, Error> {
sqlx::query_as!(
CalendarObjectRow,
"SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
"SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
principal,
cal_id,
object_id,
@@ -349,12 +398,12 @@ impl SqliteCalendarStore {
#[instrument]
async fn _put_object<'e, E: Executor<'e, Database = Sqlite>>(
executor: E,
principal: String,
cal_id: String,
object: CalendarObject,
principal: &str,
cal_id: &str,
object: &CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
let (object_id, ics) = (object.get_id(), object.get_ics());
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
let first_occurence = object
.get_first_occurence()
@@ -373,10 +422,11 @@ impl SqliteCalendarStore {
(if overwrite {
sqlx::query!(
"REPLACE INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
"REPLACE INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
principal,
cal_id,
object_id,
uid,
ics,
first_occurence,
last_occurence,
@@ -386,10 +436,11 @@ impl SqliteCalendarStore {
} else {
// If the object already exists a database error is thrown and handled in error.rs
sqlx::query!(
"INSERT INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
"INSERT INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
principal,
cal_id,
object_id,
uid,
ics,
first_occurence,
last_occurence,
@@ -409,7 +460,7 @@ impl SqliteCalendarStore {
executor: E,
principal: &str,
cal_id: &str,
id: &str,
object_id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
if use_trashbin {
@@ -417,7 +468,7 @@ impl SqliteCalendarStore {
"UPDATE calendarobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)",
principal,
cal_id,
id
object_id
)
.execute(executor)
.await.map_err(crate::Error::from)?;
@@ -425,7 +476,7 @@ impl SqliteCalendarStore {
sqlx::query!(
"DELETE FROM calendarobjects WHERE cal_id = ? AND id = ?",
cal_id,
id
object_id
)
.execute(executor)
.await
@@ -539,7 +590,11 @@ impl CalendarStore for SqliteCalendarStore {
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
Ok(cal) => Some(cal),
@@ -573,7 +628,11 @@ impl CalendarStore for SqliteCalendarStore {
objects: Vec<CalendarObject>,
merge_existing: bool,
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let existing_cal =
match Self::_get_calendar(&mut *tx, &calendar.principal, &calendar.id, true).await {
@@ -588,18 +647,35 @@ impl CalendarStore for SqliteCalendarStore {
Self::_insert_calendar(&mut *tx, calendar.clone()).await?;
}
let mut sync_token = None;
for object in objects {
Self::_put_object(
&mut *tx,
calendar.principal.clone(),
calendar.id.clone(),
object,
false,
Self::_put_object(&mut *tx, &calendar.principal, &calendar.id, &object, false).await?;
sync_token = Some(
log_object_operation(
&mut tx,
&calendar.principal,
&calendar.id,
object.get_id(),
ChangeOperation::Add,
)
.await?;
.await?,
);
}
tx.commit().await.map_err(crate::Error::from)?;
if let Some(sync_token) = sync_token
&& let Err(err) = self.sender.try_send(CollectionOperation {
data: CollectionOperationInfo::Content { sync_token },
topic: self
.get_calendar(&calendar.principal, &calendar.id, true)
.await?
.push_topic,
})
{
error!("Push notification about imported calendar failed: {err}");
}
Ok(())
}
@@ -663,7 +739,11 @@ impl CalendarStore for SqliteCalendarStore {
object: CalendarObject,
overwrite: bool,
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
let object_id = object.get_id().to_owned();
@@ -673,14 +753,7 @@ impl CalendarStore for SqliteCalendarStore {
return Err(Error::ReadOnly);
}
Self::_put_object(
&mut *tx,
principal.clone(),
cal_id.clone(),
object,
overwrite,
)
.await?;
Self::_put_object(&mut *tx, &principal, &cal_id, &object, overwrite).await?;
let sync_token = log_object_operation(
&mut tx,
@@ -713,7 +786,11 @@ impl CalendarStore for SqliteCalendarStore {
id: &str,
use_trashbin: bool,
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
@@ -737,7 +814,11 @@ impl CalendarStore for SqliteCalendarStore {
cal_id: &str,
object_id: &str,
) -> Result<(), Error> {
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
let mut tx = self
.db
.begin_with(BEGIN_IMMEDIATE)
.await
.map_err(crate::Error::from)?;
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
@@ -750,7 +831,7 @@ impl CalendarStore for SqliteCalendarStore {
data: CollectionOperationInfo::Content { sync_token },
topic: self.get_calendar(principal, cal_id, true).await?.push_topic,
}) {
error!("Push notification about deleted calendar failed: {err}");
error!("Push notification about restored calendar object failed: {err}");
}
Ok(())
}
@@ -771,6 +852,7 @@ impl CalendarStore for SqliteCalendarStore {
}
// Logs an operation to the events
// TODO: Log multiple updates
async fn log_object_operation(
tx: &mut Transaction<'_, Sqlite>,
principal: &str,

View File

@@ -10,6 +10,9 @@ pub mod error;
pub mod principal_store;
pub mod subscription_store;
// Begin statement for write transactions
pub const BEGIN_IMMEDIATE: &str = "BEGIN IMMEDIATE";
#[cfg(any(test, feature = "test"))]
pub mod tests;

View File

@@ -0,0 +1,81 @@
#[cfg(test)]
mod tests {
use crate::{addressbook_store::SqliteAddressbookStore, tests::get_test_addressbook_store};
use rstest::rstest;
use rustical_store::{Addressbook, AddressbookStore};
#[rstest]
#[tokio::test]
async fn test_addressbook_store(
#[from(get_test_addressbook_store)]
#[future]
addr_store: SqliteAddressbookStore,
) {
let addr_store = addr_store.await;
let cal = Addressbook {
id: "addr".to_string(),
principal: "fake-user".to_string(),
displayname: None,
description: None,
deleted_at: None,
synctoken: 0,
push_topic: "alskdj".to_string(),
};
assert!(
addr_store.insert_addressbook(cal).await.is_err(),
"This should fail due to the user not existing "
);
let addr = Addressbook {
id: "addr".to_string(),
principal: "user".to_string(),
displayname: None,
description: None,
deleted_at: None,
synctoken: 0,
push_topic: "alskdj".to_string(),
};
addr_store.insert_addressbook(addr.clone()).await.unwrap();
assert_eq!(
addr_store
.get_addressbook("user", "addr", false)
.await
.unwrap(),
addr
);
addr_store
.delete_addressbook("user", "addr", true)
.await
.unwrap();
let Err(err) = addr_store.get_addressbook("user", "addr", false).await else {
panic!()
};
assert!(err.is_not_found());
addr_store
.get_addressbook("user", "addr", true)
.await
.unwrap();
addr_store
.restore_addressbook("user", "addr")
.await
.unwrap();
addr_store
.delete_addressbook("user", "addr", false)
.await
.unwrap();
let Err(err) = addr_store.get_addressbook("user", "addr", true).await else {
panic!()
};
assert!(err.is_not_found());
}
}

View File

@@ -0,0 +1,76 @@
#[cfg(test)]
mod tests {
use crate::{calendar_store::SqliteCalendarStore, tests::get_test_calendar_store};
use rstest::rstest;
use rustical_store::{Calendar, CalendarMetadata, CalendarStore};
#[rstest]
#[tokio::test]
async fn test_calendar_store(
#[from(get_test_calendar_store)]
#[future]
cal_store: SqliteCalendarStore,
) {
let cal_store = cal_store.await;
let cal = Calendar {
principal: "fake-user".to_string(),
timezone_id: None,
deleted_at: None,
meta: CalendarMetadata::default(),
id: "cal".to_string(),
synctoken: 0,
subscription_url: None,
push_topic: "alskdj".to_string(),
components: vec![],
};
assert!(
cal_store.insert_calendar(cal).await.is_err(),
"This should fail due to the user not existing "
);
let cal = Calendar {
principal: "user".to_string(),
timezone_id: None,
deleted_at: None,
meta: CalendarMetadata::default(),
id: "cal".to_string(),
synctoken: 0,
subscription_url: None,
push_topic: "alskdj".to_string(),
components: vec![],
};
cal_store.insert_calendar(cal.clone()).await.unwrap();
assert_eq!(
cal_store.get_calendar("user", "cal", false).await.unwrap(),
cal
);
cal_store
.delete_calendar("user", "cal", true)
.await
.unwrap();
let Err(err) = cal_store.get_calendar("user", "cal", false).await else {
panic!()
};
assert!(err.is_not_found());
cal_store.get_calendar("user", "cal", true).await.unwrap();
cal_store.restore_calendar("user", "cal").await.unwrap();
cal_store
.delete_calendar("user", "cal", false)
.await
.unwrap();
let Err(err) = cal_store.get_calendar("user", "cal", true).await else {
panic!()
};
assert!(err.is_not_found());
}
}

View File

@@ -8,6 +8,9 @@ use tokio::sync::OnceCell;
static DB: OnceCell<SqlitePool> = OnceCell::const_new();
mod addressbook_store;
mod calendar_store;
async fn get_test_db() -> SqlitePool {
DB.get_or_init(async || {
let db = SqlitePool::connect("sqlite::memory:").await.unwrap();

View File

@@ -1,6 +1,7 @@
[package]
name = "rustical_xml"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
description.workspace = true
repository.workspace = true

View File

@@ -1,7 +1,8 @@
[package]
name = "xml_derive"
version = "0.1.0"
edition = "2024"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
license.workspace = true
[lib]

View File

@@ -313,7 +313,7 @@ impl Field {
}
}),
(FieldType::Text, false) => Some(quote! {
writer.write_event(Event::Text(BytesText::new(&self.#target_field_index)))?;
writer.write_event(Event::Text(BytesText::new(self.#target_field_index.as_ref())))?;
}),
(FieldType::Tag, true) => {
let field_name = self.xml_name();

View File

@@ -0,0 +1 @@
google-site-verification: googlec55e08580a46745c.html

View File

@@ -173,7 +173,9 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
tracing::debug!("unauthorized");
}
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");

View File

@@ -69,7 +69,9 @@ async fn get_data_stores(
let (send, recv) = tokio::sync::mpsc::channel(1000);
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));
cal_store.repair_orphans().await?;
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
let principal_store = Arc::new(SqlitePrincipalStore::new(db));
(