Compare commits

..

34 Commits

Author SHA1 Message Date
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
79 changed files with 1984 additions and 1191 deletions

View File

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

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", "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": { "describe": {
"columns": [ "columns": [
{ {
@@ -9,18 +9,24 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "ics", "name": "uid",
"ordinal": 1, "ordinal": 1,
"type_info": "Text" "type_info": "Text"
},
{
"name": "ics",
"ordinal": 2,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
"Right": 4 "Right": 4
}, },
"nullable": [ "nullable": [
false,
false, 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", "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": { "describe": {
"columns": [ "columns": [
{ {
@@ -9,18 +9,24 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "ics", "name": "uid",
"ordinal": 1, "ordinal": 1,
"type_info": "Text" "type_info": "Text"
},
{
"name": "ics",
"ordinal": 2,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
"Right": 2 "Right": 2
}, },
"nullable": [ "nullable": [
false,
false, 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

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

255
Cargo.lock generated
View File

@@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -139,7 +139,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"serde", "serde",
"serde_derive", "serde_derive",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -175,7 +175,7 @@ checksum = "34921de3d57974069bad483fdfe0ec65d88c4ff892edd1ab4d8b03be0dda1b9b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -310,7 +310,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -397,13 +397,14 @@ dependencies = [
[[package]] [[package]]
name = "axum-extra" name = "axum-extra"
version = "0.10.3" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" checksum = "5136e6c5e7e7978fe23e9876fb924af2c0f84c72127ac6ac17e7c46f457d362c"
dependencies = [ dependencies = [
"axum", "axum",
"axum-core", "axum-core",
"bytes", "bytes",
"futures-core",
"futures-util", "futures-util",
"headers", "headers",
"http", "http",
@@ -411,8 +412,6 @@ dependencies = [
"http-body-util", "http-body-util",
"mime", "mime",
"pin-project-lite", "pin-project-lite",
"rustversion",
"serde_core",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -532,9 +531,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.43" version = "1.2.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"shlex", "shlex",
@@ -587,9 +586,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.50" version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623" checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -597,9 +596,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.50" version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0" checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@@ -616,7 +615,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -748,7 +747,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -772,7 +771,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -783,7 +782,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -824,7 +823,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
"unicode-xid", "unicode-xid",
] ]
@@ -848,7 +847,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -1179,7 +1178,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -1569,7 +1568,7 @@ dependencies = [
[[package]] [[package]]
name = "ical" name = "ical"
version = "0.11.0" 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 = [ dependencies = [
"chrono", "chrono",
"chrono-tz", "chrono-tz",
@@ -1582,9 +1581,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.0.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"potential_utf", "potential_utf",
@@ -1595,9 +1594,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_locale_core" name = "icu_locale_core"
version = "2.0.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"litemap", "litemap",
@@ -1608,11 +1607,10 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer" name = "icu_normalizer"
version = "2.0.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
dependencies = [ dependencies = [
"displaydoc",
"icu_collections", "icu_collections",
"icu_normalizer_data", "icu_normalizer_data",
"icu_properties", "icu_properties",
@@ -1623,42 +1621,38 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer_data" name = "icu_normalizer_data"
version = "2.0.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
version = "2.0.1" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
dependencies = [ dependencies = [
"displaydoc",
"icu_collections", "icu_collections",
"icu_locale_core", "icu_locale_core",
"icu_properties_data", "icu_properties_data",
"icu_provider", "icu_provider",
"potential_utf",
"zerotrie", "zerotrie",
"zerovec", "zerovec",
] ]
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "2.0.1" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
version = "2.0.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_locale_core", "icu_locale_core",
"stable_deref_trait",
"tinystr",
"writeable", "writeable",
"yoke", "yoke",
"zerofrom", "zerofrom",
@@ -1730,9 +1724,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]] [[package]]
name = "iri-string" name = "iri-string"
version = "0.7.8" version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@@ -1838,9 +1832,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -1963,11 +1957,10 @@ dependencies = [
[[package]] [[package]]
name = "num-bigint-dig" name = "num-bigint-dig"
version = "0.8.4" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b"
dependencies = [ dependencies = [
"byteorder",
"lazy_static", "lazy_static",
"libm", "libm",
"num-integer", "num-integer",
@@ -2109,7 +2102,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -2334,7 +2327,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -2407,7 +2400,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -2476,9 +2469,9 @@ dependencies = [
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [ dependencies = [
"zerovec", "zerovec",
] ]
@@ -2533,7 +2526,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
"version_check", "version_check",
"yansi", "yansi",
] ]
@@ -2558,7 +2551,7 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -2725,7 +2718,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -2899,7 +2892,7 @@ dependencies = [
"regex", "regex",
"relative-path", "relative-path",
"rustc_version", "rustc_version",
"syn 2.0.108", "syn 2.0.109",
"unicode-ident", "unicode-ident",
] ]
@@ -2911,7 +2904,7 @@ checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14"
dependencies = [ dependencies = [
"quote", "quote",
"rand 0.8.5", "rand 0.8.5",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -2926,9 +2919,9 @@ dependencies = [
[[package]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.8.0" version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb44e1917075637ee8c7bcb865cf8830e3a92b5b1189e44e3a0ab5a0d5be314b" checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca"
dependencies = [ dependencies = [
"rust-embed-impl", "rust-embed-impl",
"rust-embed-utils", "rust-embed-utils",
@@ -2937,22 +2930,22 @@ dependencies = [
[[package]] [[package]]
name = "rust-embed-impl" name = "rust-embed-impl"
version = "8.8.0" version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382499b49db77a7c19abd2a574f85ada7e9dbe125d5d1160fa5cad7c4cf71fc9" checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.108", "syn 2.0.109",
"walkdir", "walkdir",
] ]
[[package]] [[package]]
name = "rust-embed-utils" name = "rust-embed-utils"
version = "8.8.0" version = "8.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21fcbee55c2458836bcdbfffb6ec9ba74bbc23ca7aa6816015a3dd2c4d8fc185" checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475"
dependencies = [ dependencies = [
"sha2", "sha2",
"walkdir", "walkdir",
@@ -2981,7 +2974,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3024,7 +3017,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3064,7 +3057,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3096,7 +3089,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3121,7 +3114,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3146,7 +3139,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3159,6 +3152,7 @@ dependencies = [
"headers", "headers",
"hex", "hex",
"http", "http",
"itertools 0.14.0",
"mime_guess", "mime_guess",
"percent-encoding", "percent-encoding",
"rand 0.9.2", "rand 0.9.2",
@@ -3167,6 +3161,7 @@ dependencies = [
"rustical_oidc", "rustical_oidc",
"rustical_store", "rustical_store",
"serde", "serde",
"serde_json",
"thiserror 2.0.17", "thiserror 2.0.17",
"tokio", "tokio",
"tower", "tower",
@@ -3175,11 +3170,12 @@ dependencies = [
"tracing", "tracing",
"url", "url",
"uuid", "uuid",
"vtimezones-rs",
] ]
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3196,7 +3192,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3212,7 +3208,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3245,7 +3241,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3266,7 +3262,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.9.13" version = "0.10.2"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -3288,9 +3284,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.34" version = "0.23.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"ring", "ring",
@@ -3312,9 +3308,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.103.7" version = "0.103.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
dependencies = [ dependencies = [
"ring", "ring",
"rustls-pki-types", "rustls-pki-types",
@@ -3356,9 +3352,9 @@ dependencies = [
[[package]] [[package]]
name = "schemars" name = "schemars"
version = "1.0.4" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
dependencies = [ dependencies = [
"dyn-clone", "dyn-clone",
"ref-cast", "ref-cast",
@@ -3429,7 +3425,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3507,7 +3503,7 @@ dependencies = [
"indexmap 1.9.3", "indexmap 1.9.3",
"indexmap 2.12.0", "indexmap 2.12.0",
"schemars 0.9.0", "schemars 0.9.0",
"schemars 1.0.4", "schemars 1.1.0",
"serde_core", "serde_core",
"serde_json", "serde_json",
"serde_with_macros", "serde_with_macros",
@@ -3523,7 +3519,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3691,7 +3687,7 @@ dependencies = [
"quote", "quote",
"sqlx-core", "sqlx-core",
"sqlx-macros-core", "sqlx-macros-core",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3714,7 +3710,7 @@ dependencies = [
"sqlx-mysql", "sqlx-mysql",
"sqlx-postgres", "sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 2.0.108", "syn 2.0.109",
"tokio", "tokio",
"url", "url",
] ]
@@ -3866,7 +3862,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3888,9 +3884,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.108" version = "2.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3914,7 +3910,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3943,7 +3939,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3954,7 +3950,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -3999,9 +3995,9 @@ dependencies = [
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"zerovec", "zerovec",
@@ -4048,7 +4044,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -4074,9 +4070,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-util" name = "tokio-util"
version = "0.7.16" version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-core",
@@ -4360,7 +4356,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -4456,24 +4452,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.20" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.24" version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]] [[package]]
name = "unicode-properties" name = "unicode-properties"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
@@ -4643,7 +4639,7 @@ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -4678,9 +4674,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e"
dependencies = [ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
@@ -4725,7 +4721,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -4736,7 +4732,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -5011,20 +5007,20 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]] [[package]]
name = "writeable" name = "writeable"
version = "0.6.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "xml_derive" name = "xml_derive"
version = "0.1.0" version = "0.10.2"
dependencies = [ dependencies = [
"darling", "darling",
"heck", "heck",
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -5035,11 +5031,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [ dependencies = [
"serde",
"stable_deref_trait", "stable_deref_trait",
"yoke-derive", "yoke-derive",
"zerofrom", "zerofrom",
@@ -5047,13 +5042,13 @@ dependencies = [
[[package]] [[package]]
name = "yoke-derive" name = "yoke-derive"
version = "0.8.0" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
"synstructure", "synstructure",
] ]
@@ -5074,7 +5069,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]
[[package]] [[package]]
@@ -5094,7 +5089,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
"synstructure", "synstructure",
] ]
@@ -5106,9 +5101,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"yoke", "yoke",
@@ -5117,9 +5112,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.11.4" version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [ dependencies = [
"yoke", "yoke",
"zerofrom", "zerofrom",
@@ -5128,11 +5123,11 @@ dependencies = [
[[package]] [[package]]
name = "zerovec-derive" name = "zerovec-derive"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.108", "syn 2.0.109",
] ]

View File

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

View File

@@ -1,6 +1,7 @@
[package] [package]
name = "rustical_caldav" name = "rustical_caldav"
version.workspace = true version.workspace = true
rust-version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
@@ -17,21 +18,21 @@ serde_json.workspace = true
axum.workspace = true axum.workspace = true
axum-extra.workspace = true axum-extra.workspace = true
tower.workspace = true tower.workspace = true
async-trait = { workspace = true } async-trait.workspace = true
thiserror = { workspace = true } thiserror.workspace = true
quick-xml = { workspace = true } quick-xml.workspace = true
tracing = { workspace = true } tracing.workspace = true
futures-util = { workspace = true } futures-util.workspace = true
derive_more = { workspace = true } derive_more.workspace = true
base64 = { workspace = true } base64.workspace = true
serde = { workspace = true } serde.workspace = true
tokio = { workspace = true } tokio.workspace = true
url = { workspace = true } url.workspace = true
rustical_dav = { workspace = true } rustical_dav.workspace = true
rustical_store = { workspace = true } rustical_store.workspace = true
chrono = { workspace = true } chrono.workspace = true
chrono-tz = { workspace = true } chrono-tz.workspace = true
sha2 = { workspace = true } sha2.workspace = true
ical.workspace = true ical.workspace = true
percent-encoding.workspace = true percent-encoding.workspace = true
rustical_xml.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 let objects = expanded_cals
.into_iter() .into_iter()
.map(|cal| cal.generate()) .map(|cal| cal.generate())
.map(CalendarObject::from_ics) .map(|ics| CalendarObject::from_ics(ics, None))
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;
let new_cal = Calendar { let new_cal = Calendar {
principal, principal,

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_dav::xml::PropfindType;
use rustical_ical::{CalendarObject, UtcDateTime}; use rustical_ical::{CalendarObject, UtcDateTime};
use rustical_store::calendar_store::CalendarQuery; use rustical_store::calendar_store::CalendarQuery;
@@ -26,112 +32,6 @@ pub struct ParamFilterElement {
pub(crate) name: String, 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)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7 // https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
@@ -142,8 +42,9 @@ pub struct FilterElement {
} }
impl FilterElement { impl FilterElement {
#[must_use]
pub fn matches(&self, cal_object: &CalendarObject) -> bool { 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() 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_ical::CalendarObject;
use rustical_store::CalendarStore; use rustical_store::CalendarStore;
mod comp_filter;
mod elements; mod elements;
mod prop_filter;
pub mod text_match;
#[allow(unused_imports)]
pub use comp_filter::{CompFilterElement, CompFilterable};
pub use elements::*; 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>( pub async fn get_objects_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest, cal_query: &CalendarQueryRequest,
@@ -29,8 +38,10 @@ mod tests {
calendar::methods::report::{ calendar::methods::report::{
ReportRequest, ReportRequest,
calendar_query::{ calendar_query::{
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement, CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
PropFilterElement, TextMatchElement, comp_filter::CompFilterElement,
prop_filter::PropFilterElement,
text_match::{NegateCondition, TextCollation},
}, },
}, },
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName}, calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
@@ -90,16 +101,18 @@ mod tests {
prop_filter: vec![PropFilterElement { prop_filter: vec![PropFilterElement {
name: "ATTENDEE".to_owned(), name: "ATTENDEE".to_owned(),
text_match: Some(TextMatchElement { text_match: Some(TextMatchElement {
collation: "i;ascii-casemap".to_owned(), collation: TextCollation::AsciiCasemap,
negate_condition: None negate_condition: NegateCondition(false),
needle: "mailto:lisa@example.com".to_string()
}), }),
is_not_defined: None, is_not_defined: None,
param_filter: vec![ParamFilterElement { param_filter: vec![ParamFilterElement {
is_not_defined: None, is_not_defined: None,
name: "PARTSTAT".to_owned(), name: "PARTSTAT".to_owned(),
text_match: Some(TextMatchElement { text_match: Some(TextMatchElement {
collation: "i;ascii-casemap".to_owned(), collation: TextCollation::AsciiCasemap,
negate_condition: None negate_condition: NegateCondition(false),
needle: "NEEDS-ACTION".to_string()
}), }),
}], }],
time_range: None 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; use tracing::instrument;
mod calendar_multiget; mod calendar_multiget;
mod calendar_query; pub mod calendar_query;
mod sync_collection; mod sync_collection;
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]

View File

@@ -3,6 +3,8 @@ use rustical_ical::CalendarObjectType;
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray; use strum_macros::VariantArray;
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)] #[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
pub struct SupportedCalendarComponent { pub struct SupportedCalendarComponent {
#[xml(ty = "attr")] #[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)] #[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)]
pub struct CalendarData { pub struct CalendarData {
#[xml(ty = "attr")] #[xml(ty = "attr")]

View File

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

View File

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

View File

@@ -11,7 +11,7 @@ use rustical_ical::CalendarObject;
use rustical_store::CalendarStore; use rustical_store::CalendarStore;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use std::str::FromStr; use std::str::FromStr;
use tracing::{debug, error, instrument}; use tracing::{debug, instrument};
#[instrument(skip(cal_store))] #[instrument(skip(cal_store))]
pub async fn get_event<C: CalendarStore>( pub async fn get_event<C: CalendarStore>(
@@ -78,18 +78,10 @@ pub async fn put_event<C: CalendarStore>(
true 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}"); debug!("invalid calendar data:\n{body}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); 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 cal_store
.put_object(principal, calendar_id, object, overwrite) .put_object(principal, calendar_id, object, overwrite)
.await?; .await?;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,11 @@ import { escapeXml } from ".";
export class CreateCalendarForm extends LitElement { export class CreateCalendarForm extends LitElement {
constructor() { constructor() {
super() super()
this.fetchTimezones()
}
async fetchTimezones() {
this.timezones = await getTimezones()
} }
protected override createRenderRoot() { protected override createRenderRoot() {
@@ -36,6 +41,8 @@ export class CreateCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef() dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef() form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() { override render() {
return html` return html`
@@ -65,7 +72,12 @@ export class CreateCalendarForm extends LitElement {
<br> <br>
<label> <label>
Timezone (optional) 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> </label>
<br> <br>
<label> <label>

View File

@@ -2,11 +2,17 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property } from "lit/decorators.js";
import { Ref, createRef, ref } from 'lit/directives/ref.js'; import { Ref, createRef, ref } from 'lit/directives/ref.js';
import { escapeXml } from "."; import { escapeXml } from ".";
import { getTimezones } from "./timezones.ts";
@customElement("edit-calendar-form") @customElement("edit-calendar-form")
export class EditCalendarForm extends LitElement { export class EditCalendarForm extends LitElement {
constructor() { constructor() {
super() super()
this.fetchTimezones()
}
async fetchTimezones() {
this.timezones = await getTimezones()
} }
protected override createRenderRoot() { protected override createRenderRoot() {
@@ -36,7 +42,8 @@ export class EditCalendarForm extends LitElement {
dialog: Ref<HTMLDialogElement> = createRef() dialog: Ref<HTMLDialogElement> = createRef()
form: Ref<HTMLFormElement> = createRef() form: Ref<HTMLFormElement> = createRef()
@property()
timezones: Array<String> = []
override render() { override render() {
return html` return html`
@@ -51,7 +58,12 @@ export class EditCalendarForm extends LitElement {
<br> <br>
<label> <label>
Timezone (optional) 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> </label>
<br> <br>
<label> <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

@@ -27,6 +27,11 @@ let CreateCalendarForm = class extends i {
this.components = /* @__PURE__ */ new Set(); this.components = /* @__PURE__ */ new Set();
this.dialog = e(); this.dialog = e();
this.form = e(); this.form = e();
this.timezones = [];
this.fetchTimezones();
}
async fetchTimezones() {
this.timezones = await getTimezones();
} }
createRenderRoot() { createRenderRoot() {
return this; return this;
@@ -59,7 +64,12 @@ let CreateCalendarForm = class extends i {
<br> <br>
<label> <label>
Timezone (optional) 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> </label>
<br> <br>
<label> <label>
@@ -179,6 +189,9 @@ __decorateClass([
__decorateClass([ __decorateClass([
n$1() n$1()
], CreateCalendarForm.prototype, "components", 2); ], CreateCalendarForm.prototype, "components", 2);
__decorateClass([
n$1()
], CreateCalendarForm.prototype, "timezones", 2);
CreateCalendarForm = __decorateClass([ CreateCalendarForm = __decorateClass([
t("create-calendar-form") t("create-calendar-form")
], CreateCalendarForm); ], CreateCalendarForm);

View File

@@ -2,6 +2,18 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
import { n as n$1, t } from "./property-B8WoKf1Y.mjs"; import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
import { e, n } from "./ref-BwbQvJBB.mjs"; import { e, n } from "./ref-BwbQvJBB.mjs";
import { e as escapeXml } from "./index-_IB1wMbZ.mjs"; import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
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 (e2) {
reject(e2);
}
});
return await timezonesPromise;
}
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __decorateClass = (decorators, target, key, kind) => { var __decorateClass = (decorators, target, key, kind) => {
@@ -22,6 +34,11 @@ let EditCalendarForm = class extends i {
this.components = /* @__PURE__ */ new Set(); this.components = /* @__PURE__ */ new Set();
this.dialog = e(); this.dialog = e();
this.form = e(); this.form = e();
this.timezones = [];
this.fetchTimezones();
}
async fetchTimezones() {
this.timezones = await getTimezones();
} }
createRenderRoot() { createRenderRoot() {
return this; return this;
@@ -39,7 +56,12 @@ let EditCalendarForm = class extends i {
<br> <br>
<label> <label>
Timezone (optional) 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> </label>
<br> <br>
<label> <label>
@@ -150,6 +172,9 @@ __decorateClass([
} }
}) })
], EditCalendarForm.prototype, "components", 2); ], EditCalendarForm.prototype, "components", 2);
__decorateClass([
n$1()
], EditCalendarForm.prototype, "timezones", 2);
EditCalendarForm = __decorateClass([ EditCalendarForm = __decorateClass([
t("edit-calendar-form") t("edit-calendar-form")
], EditCalendarForm); ], EditCalendarForm);

File diff suppressed because it is too large Load Diff

View File

@@ -79,9 +79,6 @@ header {
nav { nav {
display: flex; display: flex;
border-radius: 12px;
background: color-mix(in srgb, var(--background-darker), var(--dilute-color) 5%);
a { a {
text-decoration: none; text-decoration: none;
margin: 4px 8px; margin: 4px 8px;
@@ -259,19 +256,6 @@ ul.collection-list {
margin: 8px initial; 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 { .description {
grid-area: description; grid-area: description;
white-space: nowrap; white-space: nowrap;
@@ -363,3 +347,16 @@ svg.icon {
color: var(--text-on-background-color); color: var(--text-on-background-color);
stroke: 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"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps"> <div class="component-chips">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span class="chip">{{ comp }}</span>
{% endfor %} {% endfor %}
</div> </div>
</span> </span>
@@ -58,9 +58,9 @@
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps"> <div class="component-chips">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span class="chip">{{ comp }}</span>
{% endfor %} {% endfor %}
</div> </div>
</span> </span>
@@ -85,4 +85,3 @@
{% endif %} {% endif %}
<create-calendar-form user="{{ user.id }}"></create-calendar-form> <create-calendar-form user="{{ user.id }}"></create-calendar-form>
<import-calendar-form user="{{ user.id }}"></import-calendar-form> <import-calendar-form user="{{ user.id }}"></import-calendar-form>

View File

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

View File

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

View File

@@ -5,7 +5,15 @@
{% block content %} {% block content %}
{% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %} {% 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(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
@@ -13,19 +21,7 @@
<a href="{{ subscription_url }}">{{ subscription_url }}</a> <a href="{{ subscription_url }}">{{ subscription_url }}</a>
{% endif %} {% endif %}
<h2>Components</h2> <h2>Debug information</h2>
<ul> <pre>{{ calendar|json(2) }}</pre>
{% 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>
{%endblock %} {%endblock %}

View File

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

View File

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

View File

@@ -1 +1,8 @@
use rustical_store::auth::Principal;
pub mod user; 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 askama_web::WebTemplate;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use crate::pages::DefaultLayoutData;
pub trait Section: Template { pub trait Section: Template {
fn name() -> &'static str; fn name() -> &'static str;
} }
@@ -12,3 +14,9 @@ pub struct UserPage<S: Section> {
pub user: Principal, pub user: Principal,
pub section: S, 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 http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, auth::Principal}; use rustical_store::{Addressbook, AddressbookStore, auth::Principal};
use crate::pages::DefaultLayoutData;
#[derive(Template, WebTemplate)] #[derive(Template, WebTemplate)]
#[template(path = "pages/addressbook.html")] #[template(path = "pages/addressbook.html")]
struct AddressbookPage { struct AddressbookPage {
addressbook: Addressbook, addressbook: Addressbook,
user: Principal,
}
impl DefaultLayoutData for AddressbookPage {
fn get_user(&self) -> Option<&Principal> {
Some(&self.user)
}
} }
pub async fn route_addressbook<AS: AddressbookStore>( pub async fn route_addressbook<AS: AddressbookStore>(
@@ -28,6 +37,7 @@ pub async fn route_addressbook<AS: AddressbookStore>(
} }
Ok(AddressbookPage { Ok(AddressbookPage {
addressbook: store.get_addressbook(&owner, &addrbook_id, true).await?, addressbook: store.get_addressbook(&owner, &addrbook_id, true).await?,
user,
} }
.into_response()) .into_response())
} }

View File

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

View File

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

View File

@@ -4,4 +4,5 @@ pub mod app_token;
pub mod calendar; pub mod calendar;
pub mod calendars; pub mod calendars;
pub mod login; pub mod login;
pub mod timezones;
pub mod user; 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] [package]
name = "rustical_ical" name = "rustical_ical"
version.workspace = true version.workspace = true
rust-version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true

View File

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

View File

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

View File

@@ -251,7 +251,7 @@ END:VEVENT\r\n",
#[test] #[test]
fn test_expand_recurrence() { 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 { let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
panic!() panic!()
}; };

View File

@@ -64,6 +64,19 @@ pub enum CalendarObjectComponent {
Journal(IcalJournal, Vec<IcalJournal>), 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 { impl From<&CalendarObjectComponent> for CalendarObjectType {
fn from(value: &CalendarObjectComponent) -> Self { fn from(value: &CalendarObjectComponent) -> Self {
match value { match value {
@@ -135,18 +148,47 @@ impl CalendarObjectComponent {
} }
Ok(Self::Journal(main_journal, overrides)) 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)] #[derive(Debug, Clone)]
pub struct CalendarObject { pub struct CalendarObject {
data: CalendarObjectComponent, data: CalendarObjectComponent,
properties: Vec<Property>, properties: Vec<Property>,
id: String,
ics: String, ics: String,
vtimezones: HashMap<String, IcalTimeZone>, vtimezones: HashMap<String, IcalTimeZone>,
} }
impl CalendarObject { 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 mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
let cal = parser.next().ok_or(Error::MissingCalendar)??; let cal = parser.next().ok_or(Error::MissingCalendar)??;
if parser.next().is_some() { if parser.next().is_some() {
@@ -202,6 +244,7 @@ impl CalendarObject {
}; };
Ok(Self { Ok(Self {
id: id.unwrap_or_else(|| data.get_uid().to_owned()),
data, data,
properties: cal.properties, properties: cal.properties,
ics, ics,
@@ -219,21 +262,20 @@ impl CalendarObject {
&self.data &self.data
} }
#[must_use]
pub fn get_uid(&self) -> &str {
self.data.get_uid()
}
#[must_use] #[must_use]
pub fn get_id(&self) -> &str { pub fn get_id(&self) -> &str {
match &self.data { &self.id
// 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(),
}
} }
#[must_use] #[must_use]
pub fn get_etag(&self) -> String { pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(self.get_id()); hasher.update(self.get_uid());
hasher.update(self.get_ics()); hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize()) format!("\"{:x}\"", hasher.finalize())
} }
@@ -254,31 +296,11 @@ impl CalendarObject {
} }
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> { pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self.data { self.data.get_first_occurence()
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),
}
} }
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> { pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
match &self.data { self.data.get_last_occurence()
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),
}
} }
pub fn expand_recurrence( pub fn expand_recurrence(
@@ -299,4 +321,11 @@ impl CalendarObject {
_ => Ok(self.get_ics().to_string()), _ => 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"; const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
pub const LOCAL_DATE: &str = "%Y%m%d"; pub const LOCAL_DATE: &str = "%Y%m%d";
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum CalDateTimeError { pub enum CalDateTimeError {
#[error( #[error(
"Timezone has X-LIC-LOCATION property to specify a timezone from the Olson database, however its value {0} is invalid" "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] #[test]
fn parse_calendar_object() { 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(); object.expand_recurrence(None, None).unwrap();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
[package] [package]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version.workspace = true version.workspace = true
rust-version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
@@ -15,12 +16,12 @@ rstest.workspace = true
[dependencies] [dependencies]
tokio.workspace = true tokio.workspace = true
rustical_store = { workspace = true } rustical_store.workspace = true
async-trait = { workspace = true } async-trait.workspace = true
serde = { workspace = true } serde.workspace = true
sqlx = { workspace = true } sqlx.workspace = true
thiserror = { workspace = true } thiserror.workspace = true
tracing = { workspace = true } tracing.workspace = true
derive_more.workspace = true derive_more.workspace = true
chrono.workspace = true chrono.workspace = true
password-auth.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 super::ChangeOperation;
use crate::BEGIN_IMMEDIATE;
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use rustical_ical::AddressObject; use rustical_ical::AddressObject;
@@ -414,7 +415,11 @@ impl AddressbookStore for SqliteAddressbookStore {
addressbook_id: &str, addressbook_id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), rustical_store::Error> { ) -> 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 = let addressbook =
match Self::_get_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await { match Self::_get_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await {
@@ -508,7 +513,11 @@ impl AddressbookStore for SqliteAddressbookStore {
object: AddressObject, object: AddressObject,
overwrite: bool, overwrite: bool,
) -> Result<(), rustical_store::Error> { ) -> 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(); let object_id = object.get_id().to_owned();
@@ -554,7 +563,11 @@ impl AddressbookStore for SqliteAddressbookStore {
object_id: &str, object_id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), rustical_store::Error> { ) -> 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?; Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
@@ -589,7 +602,11 @@ impl AddressbookStore for SqliteAddressbookStore {
addressbook_id: &str, addressbook_id: &str,
object_id: &str, object_id: &str,
) -> Result<(), rustical_store::Error> { ) -> 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?; Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
@@ -624,7 +641,11 @@ impl AddressbookStore for SqliteAddressbookStore {
objects: Vec<AddressObject>, objects: Vec<AddressObject>,
merge_existing: bool, merge_existing: bool,
) -> Result<(), Error> { ) -> 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 = let existing =
match Self::_get_addressbook(&mut *tx, &addressbook.principal, &addressbook.id, true) match Self::_get_addressbook(&mut *tx, &addressbook.principal, &addressbook.id, true)

View File

@@ -1,4 +1,5 @@
use super::ChangeOperation; use super::ChangeOperation;
use crate::BEGIN_IMMEDIATE;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::TimeDelta; use chrono::TimeDelta;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
@@ -16,19 +17,20 @@ use tracing::{error, instrument};
struct CalendarObjectRow { struct CalendarObjectRow {
id: String, id: String,
ics: String, ics: String,
uid: String,
} }
impl TryFrom<CalendarObjectRow> for CalendarObject { impl TryFrom<CalendarObjectRow> for CalendarObject {
type Error = rustical_store::Error; type Error = rustical_store::Error;
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> { fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
let object = Self::from_ics(value.ics)?; let object = Self::from_ics(value.ics, Some(value.id))?;
if object.get_id() != value.id { if object.get_uid() != value.uid {
return Err(rustical_store::Error::IcalError( return Err(rustical_store::Error::IcalError(
rustical_ical::Error::InvalidData(format!( rustical_ical::Error::InvalidData(format!(
"object_id={} and UID={} don't match", "uid={} and UID={} don't match",
object.get_id(), value.uid,
value.id object.get_uid()
)), )),
)); ));
} }
@@ -280,7 +282,7 @@ impl SqliteCalendarStore {
) -> Result<Vec<CalendarObject>, Error> { ) -> Result<Vec<CalendarObject>, Error> {
sqlx::query_as!( sqlx::query_as!(
CalendarObjectRow, 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, principal,
cal_id cal_id
) )
@@ -305,7 +307,7 @@ impl SqliteCalendarStore {
sqlx::query_as!( sqlx::query_as!(
CalendarObjectRow, CalendarObjectRow,
r"SELECT id, ics FROM calendarobjects r"SELECT id, uid, ics FROM calendarobjects
WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL
AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?)) AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))
AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?)) AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))
@@ -334,7 +336,7 @@ impl SqliteCalendarStore {
) -> Result<CalendarObject, Error> { ) -> Result<CalendarObject, Error> {
sqlx::query_as!( sqlx::query_as!(
CalendarObjectRow, 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, principal,
cal_id, cal_id,
object_id, object_id,
@@ -354,7 +356,7 @@ impl SqliteCalendarStore {
object: CalendarObject, object: CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> 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 let first_occurence = object
.get_first_occurence() .get_first_occurence()
@@ -373,10 +375,11 @@ impl SqliteCalendarStore {
(if overwrite { (if overwrite {
sqlx::query!( 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, principal,
cal_id, cal_id,
object_id, object_id,
uid,
ics, ics,
first_occurence, first_occurence,
last_occurence, last_occurence,
@@ -386,10 +389,11 @@ impl SqliteCalendarStore {
} else { } else {
// If the object already exists a database error is thrown and handled in error.rs // If the object already exists a database error is thrown and handled in error.rs
sqlx::query!( 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, principal,
cal_id, cal_id,
object_id, object_id,
uid,
ics, ics,
first_occurence, first_occurence,
last_occurence, last_occurence,
@@ -409,7 +413,7 @@ impl SqliteCalendarStore {
executor: E, executor: E,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
id: &str, object_id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
if use_trashbin { if use_trashbin {
@@ -417,7 +421,7 @@ impl SqliteCalendarStore {
"UPDATE calendarobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)", "UPDATE calendarobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)",
principal, principal,
cal_id, cal_id,
id object_id
) )
.execute(executor) .execute(executor)
.await.map_err(crate::Error::from)?; .await.map_err(crate::Error::from)?;
@@ -425,7 +429,7 @@ impl SqliteCalendarStore {
sqlx::query!( sqlx::query!(
"DELETE FROM calendarobjects WHERE cal_id = ? AND id = ?", "DELETE FROM calendarobjects WHERE cal_id = ? AND id = ?",
cal_id, cal_id,
id object_id
) )
.execute(executor) .execute(executor)
.await .await
@@ -539,7 +543,11 @@ impl CalendarStore for SqliteCalendarStore {
id: &str, id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), Error> { ) -> 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 { let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
Ok(cal) => Some(cal), Ok(cal) => Some(cal),
@@ -573,7 +581,11 @@ impl CalendarStore for SqliteCalendarStore {
objects: Vec<CalendarObject>, objects: Vec<CalendarObject>,
merge_existing: bool, merge_existing: bool,
) -> Result<(), Error> { ) -> 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 = let existing_cal =
match Self::_get_calendar(&mut *tx, &calendar.principal, &calendar.id, true).await { match Self::_get_calendar(&mut *tx, &calendar.principal, &calendar.id, true).await {
@@ -663,7 +675,11 @@ impl CalendarStore for SqliteCalendarStore {
object: CalendarObject, object: CalendarObject,
overwrite: bool, overwrite: bool,
) -> Result<(), Error> { ) -> 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(); let object_id = object.get_id().to_owned();
@@ -713,7 +729,11 @@ impl CalendarStore for SqliteCalendarStore {
id: &str, id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), Error> { ) -> 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?; Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
@@ -737,7 +757,11 @@ impl CalendarStore for SqliteCalendarStore {
cal_id: &str, cal_id: &str,
object_id: &str, object_id: &str,
) -> Result<(), Error> { ) -> 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?; Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;

View File

@@ -10,6 +10,9 @@ pub mod error;
pub mod principal_store; pub mod principal_store;
pub mod subscription_store; pub mod subscription_store;
// Begin statement for write transactions
pub const BEGIN_IMMEDIATE: &str = "BEGIN IMMEDIATE";
#[cfg(any(test, feature = "test"))] #[cfg(any(test, feature = "test"))]
pub mod tests; 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(); static DB: OnceCell<SqlitePool> = OnceCell::const_new();
mod addressbook_store;
mod calendar_store;
async fn get_test_db() -> SqlitePool { async fn get_test_db() -> SqlitePool {
DB.get_or_init(async || { DB.get_or_init(async || {
let db = SqlitePool::connect("sqlite::memory:").await.unwrap(); let db = SqlitePool::connect("sqlite::memory:").await.unwrap();

View File

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

View File

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

View File

@@ -313,7 +313,7 @@ impl Field {
} }
}), }),
(FieldType::Text, false) => Some(quote! { (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) => { (FieldType::Tag, true) => {
let field_name = self.xml_name(); 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"); tracing::debug!("unauthorized");
} }
StatusCode::NOT_FOUND => { StatusCode::NOT_FOUND => {
tracing::warn!("client error"); // Clients like GNOME Calendar will try to reach /remote.php/webdav
// quite often clogging up the logs
tracing::info!("client error");
} }
_ => { _ => {
tracing::error!("client error"); tracing::error!("client error");