mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 15:52:38 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9538b68e77 | ||
|
|
ea5175387b | ||
|
|
0095491a20 | ||
|
|
e9392cc00b | ||
|
|
888591c952 | ||
|
|
de77223170 | ||
|
|
c42f8e5614 | ||
|
|
f72559d027 | ||
|
|
167492318f | ||
|
|
32f43951ac | ||
|
|
cd9993cd97 | ||
|
|
9f911fe5d7 | ||
|
|
6361907152 | ||
|
|
0c0be859f9 | ||
|
|
d2c786eba6 | ||
|
|
dabddc6282 | ||
|
|
76b4194b94 | ||
|
|
db144ebcae | ||
|
|
a53c333f1f | ||
|
|
a05baea472 | ||
|
|
f34f7e420e | ||
|
|
24ab323aa0 | ||
|
|
f34f56ca89 | ||
|
|
8ed4db5824 |
32
.sqlx/query-3a29efff3d3f6e1e05595d1a2d095af5fc963572c90bd10a6616af78757f8c39.json
generated
Normal file
32
.sqlx/query-3a29efff3d3f6e1e05595d1a2d095af5fc963572c90bd10a6616af78757f8c39.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
"query": "SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -9,18 +9,24 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"name": "uid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "543838c030550cb09d1af08adfeade8b7ce3575d92fddbc6e9582d141bc9e49d"
|
||||
"hash": "505ebe8e64ac709b230dce7150240965e45442aca6c5f3b3115738ef508939ed"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
"query": "SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -9,18 +9,24 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"name": "uid",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "ics",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "54c9c0e36a52e6963f11c6aa27f13aafb4204b8aa34b664fd825bd447db80e86"
|
||||
"hash": "804ed2a4a7032e9605d1871297498f5a96de0fc816ce660c705fb28318be0d42"
|
||||
}
|
||||
12
.sqlx/query-a68a1b96189b854a7ba2a3cd866ba583af5ad84bc1cd8b20cb805e9ce3bad820.json
generated
Normal file
12
.sqlx/query-a68a1b96189b854a7ba2a3cd866ba583af5ad84bc1cd8b20cb805e9ce3bad820.json
generated
Normal 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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
12
.sqlx/query-d498a758ed707408b00b7d2675250ea739a681ce1f009f05e97f2e101bd7e556.json
generated
Normal file
12
.sqlx/query-d498a758ed707408b00b7d2675250ea739a681ce1f009f05e97f2e101bd7e556.json
generated
Normal 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"
|
||||
}
|
||||
139
Cargo.lock
generated
139
Cargo.lock
generated
@@ -19,9 +19,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -397,13 +397,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-extra"
|
||||
version = "0.10.3"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"
|
||||
checksum = "460c45604cb25834835e3b4d3468510322852783dac36261d642424d75562ff3"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"headers",
|
||||
"http",
|
||||
@@ -411,8 +412,6 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"rustversion",
|
||||
"serde_core",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -532,9 +531,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.43"
|
||||
version = "1.2.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2"
|
||||
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -587,9 +586,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.50"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c2cfd7bf8a6017ddaa4e32ffe7403d547790db06bd171c1c53926faab501623"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -597,9 +596,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.50"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a4c05b9e80c5ccd3a7ef080ad7b6ba7d6fc00a985b8b157197075677c82c7a0"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1582,9 +1581,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
|
||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
@@ -1595,9 +1594,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
|
||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
@@ -1608,11 +1607,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
|
||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
@@ -1623,42 +1621,38 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
|
||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.0.1"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
|
||||
checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locale_core",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.0.1"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
|
||||
checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.0.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
|
||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -1838,9 +1832,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
|
||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
@@ -1963,11 +1957,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
|
||||
checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"libm",
|
||||
"num-integer",
|
||||
@@ -2476,9 +2469,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a"
|
||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||
dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
@@ -2981,7 +2974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -3024,7 +3017,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_caldav"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"async-trait",
|
||||
@@ -3064,7 +3057,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_carddav"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3096,7 +3089,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3121,7 +3114,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_dav_push"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3146,7 +3139,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_frontend"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"askama_web",
|
||||
@@ -3159,6 +3152,7 @@ dependencies = [
|
||||
"headers",
|
||||
"hex",
|
||||
"http",
|
||||
"itertools 0.14.0",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"rand 0.9.2",
|
||||
@@ -3167,6 +3161,7 @@ dependencies = [
|
||||
"rustical_oidc",
|
||||
"rustical_store",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tower",
|
||||
@@ -3175,11 +3170,12 @@ dependencies = [
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
"vtimezones-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustical_ical"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
@@ -3196,7 +3192,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_oidc"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
@@ -3212,7 +3208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3245,7 +3241,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_store_sqlite"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"chrono",
|
||||
@@ -3266,7 +3262,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustical_xml"
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"thiserror 2.0.17",
|
||||
@@ -3312,9 +3308,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.7"
|
||||
version = "0.103.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf"
|
||||
checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -3999,9 +3995,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
|
||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
@@ -4456,24 +4452,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.20"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -5011,9 +5007,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "xml_derive"
|
||||
@@ -5035,11 +5031,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
|
||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
@@ -5047,9 +5042,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5106,9 +5101,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
|
||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
@@ -5117,9 +5112,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.4"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -5128,9 +5123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.9.13"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
description = "A CalDAV server"
|
||||
documentation = "https://lennart-k.github.io/rustical/"
|
||||
@@ -121,7 +121,7 @@ rustical_ical = { path = "./crates/ical/" }
|
||||
chrono-tz = "0.10"
|
||||
chrono-humanize = "0.2"
|
||||
rand = "0.9"
|
||||
axum-extra = { version = "0.10", features = ["typed-header"] }
|
||||
axum-extra = { version = "0.12", features = ["typed-header"] }
|
||||
rrule = "0.14"
|
||||
argon2 = "0.5"
|
||||
rpassword = "7.3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM rust:1.90-alpine AS chef
|
||||
FROM --platform=$BUILDPLATFORM rust:1.91-alpine AS chef
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
|
||||
let objects = expanded_cals
|
||||
.into_iter()
|
||||
.map(|cal| cal.generate())
|
||||
.map(CalendarObject::from_ics)
|
||||
.map(|ics| CalendarObject::from_ics(ics, None))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let new_cal = Calendar {
|
||||
principal,
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
use crate::calendar_object::CalendarObjectPropWrapperName;
|
||||
use crate::{
|
||||
calendar::methods::report::calendar_query::{
|
||||
TextMatchElement,
|
||||
comp_filter::{CompFilterElement, CompFilterable},
|
||||
},
|
||||
calendar_object::CalendarObjectPropWrapperName,
|
||||
};
|
||||
use rustical_dav::xml::PropfindType;
|
||||
use rustical_ical::{CalendarObject, UtcDateTime};
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
@@ -26,112 +32,6 @@ pub struct ParamFilterElement {
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
pub struct TextMatchElement {
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) collation: String,
|
||||
#[xml(ty = "attr")]
|
||||
// "yes" or "no", default: "no"
|
||||
pub(crate) negate_condition: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
|
||||
#[allow(dead_code)]
|
||||
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
|
||||
pub struct PropFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) text_match: Option<TextMatchElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) param_filter: Vec<ParamFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7.1
|
||||
pub struct CompFilterElement {
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) is_not_defined: Option<()>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
pub(crate) time_range: Option<TimeRangeElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) prop_filter: Vec<PropFilterElement>,
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", flatten)]
|
||||
pub(crate) comp_filter: Vec<CompFilterElement>,
|
||||
|
||||
#[xml(ty = "attr")]
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
impl CompFilterElement {
|
||||
// match the VCALENDAR part
|
||||
pub fn matches_root(&self, cal_object: &CalendarObject) -> bool {
|
||||
let comp_vcal = self.name == "VCALENDAR";
|
||||
match (self.is_not_defined, comp_vcal) {
|
||||
// Client wants VCALENDAR to not exist but we are a VCALENDAR
|
||||
(Some(()), true) |
|
||||
// Client is asking for something different than a vcalendar
|
||||
(None, false) => return false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if self.time_range.is_some() {
|
||||
// <time-range> should be applied on VEVENT/VTODO but not on VCALENDAR
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Implement prop-filter at some point
|
||||
|
||||
// Apply sub-comp-filters on VEVENT/VTODO/VJOURNAL component
|
||||
if self
|
||||
.comp_filter
|
||||
.iter()
|
||||
.all(|filter| filter.matches(cal_object))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
// match the VEVENT/VTODO/VJOURNAL part
|
||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
||||
let comp_name_matches = self.name == cal_object.get_component_name();
|
||||
match (self.is_not_defined, comp_name_matches) {
|
||||
// Client wants VCALENDAR to not exist but we are a VCALENDAR
|
||||
(Some(()), true) |
|
||||
// Client is asking for something different than a vcalendar
|
||||
(None, false) => return false,
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// TODO: Implement prop-filter (and comp-filter?) at some point
|
||||
|
||||
if let Some(time_range) = &self.time_range {
|
||||
if let Some(start) = &time_range.start
|
||||
&& let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None)
|
||||
&& **start > last_occurence.utc()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(end) = &time_range.end
|
||||
&& let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
|
||||
&& **end < first_occurence.utc()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
// https://datatracker.ietf.org/doc/html/rfc4791#section-9.7
|
||||
@@ -142,8 +42,9 @@ pub struct FilterElement {
|
||||
}
|
||||
|
||||
impl FilterElement {
|
||||
#[must_use]
|
||||
pub fn matches(&self, cal_object: &CalendarObject) -> bool {
|
||||
self.comp_filter.matches_root(cal_object)
|
||||
cal_object.matches(&self.comp_filter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,3 +88,45 @@ impl From<&CalendarQueryRequest> for CalendarQuery {
|
||||
value.filter.as_ref().map(Self::from).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::calendar::methods::report::calendar_query::{
|
||||
CompFilterElement, FilterElement, TimeRangeElement,
|
||||
};
|
||||
use chrono::{NaiveDate, TimeZone, Utc};
|
||||
use rustical_ical::UtcDateTime;
|
||||
use rustical_store::calendar_store::CalendarQuery;
|
||||
|
||||
#[test]
|
||||
fn test_filter_element_calendar_query() {
|
||||
let filter = FilterElement {
|
||||
comp_filter: CompFilterElement {
|
||||
name: "VCALENDAR".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: None,
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![CompFilterElement {
|
||||
name: "VEVENT".to_string(),
|
||||
is_not_defined: None,
|
||||
time_range: Some(TimeRangeElement {
|
||||
start: Some(UtcDateTime(
|
||||
Utc.with_ymd_and_hms(2024, 4, 1, 0, 0, 0).unwrap(),
|
||||
)),
|
||||
end: Some(UtcDateTime(
|
||||
Utc.with_ymd_and_hms(2024, 8, 1, 0, 0, 0).unwrap(),
|
||||
)),
|
||||
}),
|
||||
prop_filter: vec![],
|
||||
comp_filter: vec![],
|
||||
}],
|
||||
},
|
||||
};
|
||||
let derived_query: CalendarQuery = (&filter).into();
|
||||
let query = CalendarQuery {
|
||||
time_start: Some(NaiveDate::from_ymd_opt(2024, 4, 1).unwrap()),
|
||||
time_end: Some(NaiveDate::from_ymd_opt(2024, 8, 1).unwrap()),
|
||||
};
|
||||
assert_eq!(derived_query, query);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,17 @@ use crate::Error;
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
|
||||
mod comp_filter;
|
||||
mod elements;
|
||||
mod prop_filter;
|
||||
pub mod text_match;
|
||||
#[allow(unused_imports)]
|
||||
pub use comp_filter::{CompFilterElement, CompFilterable};
|
||||
pub use elements::*;
|
||||
#[allow(unused_imports)]
|
||||
pub use prop_filter::{PropFilterElement, PropFilterable};
|
||||
#[allow(unused_imports)]
|
||||
pub use text_match::TextMatchElement;
|
||||
|
||||
pub async fn get_objects_calendar_query<C: CalendarStore>(
|
||||
cal_query: &CalendarQueryRequest,
|
||||
@@ -29,8 +38,10 @@ mod tests {
|
||||
calendar::methods::report::{
|
||||
ReportRequest,
|
||||
calendar_query::{
|
||||
CalendarQueryRequest, CompFilterElement, FilterElement, ParamFilterElement,
|
||||
PropFilterElement, TextMatchElement,
|
||||
CalendarQueryRequest, FilterElement, ParamFilterElement, TextMatchElement,
|
||||
comp_filter::CompFilterElement,
|
||||
prop_filter::PropFilterElement,
|
||||
text_match::{NegateCondition, TextCollation},
|
||||
},
|
||||
},
|
||||
calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
|
||||
@@ -90,16 +101,18 @@ mod tests {
|
||||
prop_filter: vec![PropFilterElement {
|
||||
name: "ATTENDEE".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: "i;ascii-casemap".to_owned(),
|
||||
negate_condition: None
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "mailto:lisa@example.com".to_string()
|
||||
}),
|
||||
is_not_defined: None,
|
||||
param_filter: vec![ParamFilterElement {
|
||||
is_not_defined: None,
|
||||
name: "PARTSTAT".to_owned(),
|
||||
text_match: Some(TextMatchElement {
|
||||
collation: "i;ascii-casemap".to_owned(),
|
||||
negate_condition: None
|
||||
collation: TextCollation::AsciiCasemap,
|
||||
negate_condition: NegateCondition(false),
|
||||
needle: "NEEDS-ACTION".to_string()
|
||||
}),
|
||||
}],
|
||||
time_range: None
|
||||
|
||||
@@ -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 > ×tamp
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if let Some(UtcDateTime(end)) = end
|
||||
&& end < ×tamp
|
||||
{
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ use sync_collection::handle_sync_collection;
|
||||
use tracing::instrument;
|
||||
|
||||
mod calendar_multiget;
|
||||
mod calendar_query;
|
||||
pub mod calendar_query;
|
||||
mod sync_collection;
|
||||
|
||||
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -3,6 +3,8 @@ use rustical_ical::CalendarObjectType;
|
||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||
use strum_macros::VariantArray;
|
||||
|
||||
use crate::calendar::methods::report::calendar_query::text_match::TextCollation;
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||
pub struct SupportedCalendarComponent {
|
||||
#[xml(ty = "attr")]
|
||||
@@ -36,6 +38,28 @@ impl From<SupportedCalendarComponentSet> for Vec<CalendarObjectType> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq, From, Into)]
|
||||
pub struct SupportedCollation(#[xml(ty = "text")] pub TextCollation);
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, Eq)]
|
||||
pub struct SupportedCollationSet(
|
||||
#[xml(
|
||||
ns = "rustical_dav::namespace::NS_CALDAV",
|
||||
flatten,
|
||||
rename = "supported-collation"
|
||||
)]
|
||||
pub Vec<SupportedCollation>,
|
||||
);
|
||||
|
||||
impl Default for SupportedCollationSet {
|
||||
fn default() -> Self {
|
||||
Self(vec![
|
||||
SupportedCollation(TextCollation::AsciiCasemap),
|
||||
SupportedCollation(TextCollation::Octet),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)]
|
||||
pub struct CalendarData {
|
||||
#[xml(ty = "attr")]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
|
||||
use crate::Error;
|
||||
use crate::calendar::prop::ReportMethod;
|
||||
use crate::calendar::prop::{ReportMethod, SupportedCollationSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use derive_more::derive::{From, Into};
|
||||
use ical::IcalParser;
|
||||
@@ -39,6 +39,8 @@ pub enum CalendarProp {
|
||||
SupportedCalendarComponentSet(SupportedCalendarComponentSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||
SupportedCalendarData(SupportedCalendarData),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
|
||||
SupportedCollationSet(SupportedCollationSet),
|
||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||
MaxResourceSize(i64),
|
||||
#[xml(skip_deserializing)]
|
||||
@@ -156,6 +158,9 @@ impl Resource for CalendarResource {
|
||||
CalendarPropName::SupportedCalendarData => {
|
||||
CalendarProp::SupportedCalendarData(SupportedCalendarData::default())
|
||||
}
|
||||
CalendarPropName::SupportedCollationSet => {
|
||||
CalendarProp::SupportedCollationSet(SupportedCollationSet::default())
|
||||
}
|
||||
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000),
|
||||
CalendarPropName::SupportedReportSet => {
|
||||
CalendarProp::SupportedReportSet(SupportedReportSet::all())
|
||||
@@ -244,6 +249,7 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
CalendarProp::TimezoneServiceSet(_)
|
||||
| CalendarProp::SupportedCalendarData(_)
|
||||
| CalendarProp::SupportedCollationSet(_)
|
||||
| CalendarProp::MaxResourceSize(_)
|
||||
| CalendarProp::SupportedReportSet(_)
|
||||
| CalendarProp::Source(_)
|
||||
@@ -283,6 +289,7 @@ impl Resource for CalendarResource {
|
||||
}
|
||||
CalendarPropName::TimezoneServiceSet
|
||||
| CalendarPropName::SupportedCalendarData
|
||||
| CalendarPropName::SupportedCollationSet
|
||||
| CalendarPropName::MaxResourceSize
|
||||
| CalendarPropName::SupportedReportSet
|
||||
| CalendarPropName::Source
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<calendar-order xmlns="http://apple.com/ns/ical/"/>
|
||||
<supported-calendar-component-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
<supported-calendar-data xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
<supported-collation-set xmlns="urn:ietf:params:xml:ns:caldav"/>
|
||||
<max-resource-size xmlns="DAV:"/>
|
||||
<supported-report-set xmlns="DAV:"/>
|
||||
<source xmlns="http://calendarserver.org/ns/"/>
|
||||
@@ -160,6 +161,10 @@ END:VCALENDAR
|
||||
<CAL:supported-calendar-data>
|
||||
<CAL:calendar-data content-type="text/calendar" version="2.0"/>
|
||||
</CAL:supported-calendar-data>
|
||||
<CAL:supported-collation-set>
|
||||
<CAL:supported-collation>i;ascii-casemap</CAL:supported-collation>
|
||||
<CAL:supported-collation>i;octet</CAL:supported-collation>
|
||||
</CAL:supported-collation-set>
|
||||
<max-resource-size>10000000</max-resource-size>
|
||||
<supported-report-set>
|
||||
<supported-report>
|
||||
|
||||
@@ -11,7 +11,7 @@ use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::Principal;
|
||||
use std::str::FromStr;
|
||||
use tracing::{debug, error, instrument};
|
||||
use tracing::{debug, instrument};
|
||||
|
||||
#[instrument(skip(cal_store))]
|
||||
pub async fn get_event<C: CalendarStore>(
|
||||
@@ -78,18 +78,10 @@ pub async fn put_event<C: CalendarStore>(
|
||||
true
|
||||
};
|
||||
|
||||
let Ok(object) = CalendarObject::from_ics(body.clone()) else {
|
||||
let Ok(object) = CalendarObject::from_ics(body.clone(), Some(object_id)) else {
|
||||
debug!("invalid calendar data:\n{body}");
|
||||
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
|
||||
};
|
||||
if object.get_id() != object_id {
|
||||
error!(
|
||||
"Calendar object UID and file name not matching: UID={}, filename={}",
|
||||
object.get_id(),
|
||||
object_id
|
||||
);
|
||||
return Err(Error::PreconditionFailed(Precondition::MatchingUid));
|
||||
}
|
||||
cal_store
|
||||
.put_object(principal, calendar_id, object, overwrite)
|
||||
.await?;
|
||||
|
||||
@@ -12,8 +12,6 @@ pub enum Precondition {
|
||||
#[error("valid-calendar-data")]
|
||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||
ValidCalendarData,
|
||||
#[error("matching-uid")]
|
||||
MatchingUid,
|
||||
}
|
||||
|
||||
impl IntoResponse for Precondition {
|
||||
|
||||
@@ -11,7 +11,6 @@ publish = false
|
||||
axum.workspace = true
|
||||
tower.workspace = true
|
||||
axum-extra.workspace = true
|
||||
|
||||
rustical_xml.workspace = true
|
||||
async-trait.workspace = true
|
||||
futures-util.workspace = true
|
||||
|
||||
@@ -59,7 +59,7 @@ impl<S: SubscriptionStore> DavPushController<S> {
|
||||
let mut latest_messages = HashMap::new();
|
||||
for message in messages {
|
||||
if matches!(message.data, CollectionOperationInfo::Content { .. }) {
|
||||
latest_messages.insert(message.topic.to_string(), message);
|
||||
latest_messages.insert(message.topic.clone(), message);
|
||||
}
|
||||
}
|
||||
let messages = latest_messages.into_values();
|
||||
@@ -156,12 +156,13 @@ impl<S: SubscriptionStore> DavPushController<S> {
|
||||
) -> Result<(), NotifierError> {
|
||||
if subsciption.public_key_type != "p256dh" {
|
||||
return Err(NotifierError::InvalidPublicKeyType(
|
||||
subsciption.public_key_type.to_string(),
|
||||
subsciption.public_key_type.clone(),
|
||||
));
|
||||
}
|
||||
let endpoint = subsciption.push_resource.parse().map_err(|_| {
|
||||
NotifierError::InvalidEndpointUrl(subsciption.push_resource.to_string())
|
||||
})?;
|
||||
let endpoint = subsciption
|
||||
.push_resource
|
||||
.parse()
|
||||
.map_err(|_| NotifierError::InvalidEndpointUrl(subsciption.push_resource.clone()))?;
|
||||
let ua_public = base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(&subsciption.public_key)
|
||||
.map_err(|_| NotifierError::InvalidKeyEncoding)?;
|
||||
|
||||
@@ -39,3 +39,6 @@ headers.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
percent-encoding.workspace = true
|
||||
tower-http = { workspace = true, optional = true }
|
||||
vtimezones-rs.workspace = true
|
||||
serde_json.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
@@ -7,6 +7,11 @@ import { escapeXml } from ".";
|
||||
export class CreateCalendarForm extends LitElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.fetchTimezones()
|
||||
}
|
||||
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones()
|
||||
}
|
||||
|
||||
protected override createRenderRoot() {
|
||||
@@ -36,6 +41,8 @@ export class CreateCalendarForm extends LitElement {
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
@property()
|
||||
timezones: Array<String> = []
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
@@ -65,7 +72,12 @@ export class CreateCalendarForm extends LitElement {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map(timezone => html`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
|
||||
@@ -2,11 +2,17 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||
import { escapeXml } from ".";
|
||||
import { getTimezones } from "./timezones.ts";
|
||||
|
||||
@customElement("edit-calendar-form")
|
||||
export class EditCalendarForm extends LitElement {
|
||||
constructor() {
|
||||
super()
|
||||
this.fetchTimezones()
|
||||
}
|
||||
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones()
|
||||
}
|
||||
|
||||
protected override createRenderRoot() {
|
||||
@@ -36,7 +42,8 @@ export class EditCalendarForm extends LitElement {
|
||||
|
||||
dialog: Ref<HTMLDialogElement> = createRef()
|
||||
form: Ref<HTMLFormElement> = createRef()
|
||||
|
||||
@property()
|
||||
timezones: Array<String> = []
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
@@ -51,7 +58,12 @@ export class EditCalendarForm extends LitElement {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${e => this.timezone_id = e.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map(timezone => html`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
|
||||
12
crates/frontend/js-components/lib/timezones.ts
Normal file
12
crates/frontend/js-components/lib/timezones.ts
Normal 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
|
||||
}
|
||||
@@ -27,6 +27,11 @@ let CreateCalendarForm = class extends i {
|
||||
this.components = /* @__PURE__ */ new Set();
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
this.fetchTimezones();
|
||||
}
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones();
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -59,7 +64,12 @@ let CreateCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map((timezone) => x`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
@@ -179,6 +189,9 @@ __decorateClass([
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "components", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], CreateCalendarForm.prototype, "timezones", 2);
|
||||
CreateCalendarForm = __decorateClass([
|
||||
t("create-calendar-form")
|
||||
], CreateCalendarForm);
|
||||
|
||||
@@ -2,6 +2,18 @@ import { i, x } from "./lit-DkXrt_Iv.mjs";
|
||||
import { n as n$1, t } from "./property-B8WoKf1Y.mjs";
|
||||
import { e, n } from "./ref-BwbQvJBB.mjs";
|
||||
import { e as escapeXml } from "./index-_IB1wMbZ.mjs";
|
||||
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 __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __decorateClass = (decorators, target, key, kind) => {
|
||||
@@ -22,6 +34,11 @@ let EditCalendarForm = class extends i {
|
||||
this.components = /* @__PURE__ */ new Set();
|
||||
this.dialog = e();
|
||||
this.form = e();
|
||||
this.timezones = [];
|
||||
this.fetchTimezones();
|
||||
}
|
||||
async fetchTimezones() {
|
||||
this.timezones = await getTimezones();
|
||||
}
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -39,7 +56,12 @@ let EditCalendarForm = class extends i {
|
||||
<br>
|
||||
<label>
|
||||
Timezone (optional)
|
||||
<input type="text" name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value} />
|
||||
<select name="timezone" .value=${this.timezone_id} @change=${(e2) => this.timezone_id = e2.target.value}>
|
||||
<option value="">No timezone</option>
|
||||
${this.timezones.map((timezone) => x`
|
||||
<option value=${timezone} ?selected=${timezone === this.timezone_id}>${timezone}</option>
|
||||
`)}
|
||||
</select>
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
@@ -150,6 +172,9 @@ __decorateClass([
|
||||
}
|
||||
})
|
||||
], EditCalendarForm.prototype, "components", 2);
|
||||
__decorateClass([
|
||||
n$1()
|
||||
], EditCalendarForm.prototype, "timezones", 2);
|
||||
EditCalendarForm = __decorateClass([
|
||||
t("edit-calendar-form")
|
||||
], EditCalendarForm);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,4 +85,3 @@
|
||||
{% endif %}
|
||||
<create-calendar-form user="{{ user.id }}"></create-calendar-form>
|
||||
<import-calendar-form user="{{ user.id }}"></import-calendar-form>
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::routes::{
|
||||
app_token::{route_delete_app_token, route_post_app_token},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
timezones::route_timezones,
|
||||
user::{route_get_home, route_root, route_user_named},
|
||||
};
|
||||
#[cfg(not(feature = "dev"))]
|
||||
@@ -79,7 +80,11 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
|
||||
.route("/", get(route_root))
|
||||
.nest("/user", user_router)
|
||||
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||
.route("/logout", post(route_post_logout));
|
||||
.route("/logout", post(route_post_logout))
|
||||
.route(
|
||||
"/_timezones.json",
|
||||
get(route_timezones).head(route_timezones),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "dev"))]
|
||||
let mut router = router.route_service("/assets/{*file}", EmbedService::<Assets>::default());
|
||||
|
||||
@@ -4,4 +4,5 @@ pub mod app_token;
|
||||
pub mod calendar;
|
||||
pub mod calendars;
|
||||
pub mod login;
|
||||
pub mod timezones;
|
||||
pub mod user;
|
||||
|
||||
39
crates/frontend/src/routes/timezones.rs
Normal file
39
crates/frontend/src/routes/timezones.rs
Normal 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());
|
||||
}
|
||||
@@ -97,8 +97,9 @@ impl AddressObject {
|
||||
let uid = format!("{}-anniversary", self.get_id());
|
||||
|
||||
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
||||
Some(CalendarObject::from_ics(format!(
|
||||
r"BEGIN:VCALENDAR
|
||||
Some(CalendarObject::from_ics(
|
||||
format!(
|
||||
r"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
||||
@@ -116,7 +117,9 @@ DESCRIPTION:💍 {fullname}{year_suffix}
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR",
|
||||
))?)
|
||||
),
|
||||
None,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -136,8 +139,9 @@ END:VCALENDAR",
|
||||
let uid = format!("{}-birthday", self.get_id());
|
||||
|
||||
let year_suffix = year.map(|year| format!(" ({year})")).unwrap_or_default();
|
||||
Some(CalendarObject::from_ics(format!(
|
||||
r"BEGIN:VCALENDAR
|
||||
Some(CalendarObject::from_ics(
|
||||
format!(
|
||||
r"BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//github.com/lennart-k/rustical birthday calendar//EN
|
||||
@@ -155,7 +159,9 @@ DESCRIPTION:🎂 {fullname}{year_suffix}
|
||||
END:VALARM
|
||||
END:VEVENT
|
||||
END:VCALENDAR",
|
||||
))?)
|
||||
),
|
||||
None,
|
||||
)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
||||
@@ -251,7 +251,7 @@ END:VEVENT\r\n",
|
||||
|
||||
#[test]
|
||||
fn test_expand_recurrence() {
|
||||
let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
|
||||
let event = CalendarObject::from_ics(ICS.to_string(), None).unwrap();
|
||||
let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
@@ -64,6 +64,19 @@ pub enum CalendarObjectComponent {
|
||||
Journal(IcalJournal, Vec<IcalJournal>),
|
||||
}
|
||||
|
||||
impl CalendarObjectComponent {
|
||||
#[must_use]
|
||||
pub fn get_uid(&self) -> &str {
|
||||
match &self {
|
||||
// We've made sure before that the first component exists and all components share the
|
||||
// same UID
|
||||
Self::Todo(todo, _) => todo.get_uid(),
|
||||
Self::Event(event, _) => event.event.get_uid(),
|
||||
Self::Journal(journal, _) => journal.get_uid(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CalendarObjectComponent> for CalendarObjectType {
|
||||
fn from(value: &CalendarObjectComponent) -> Self {
|
||||
match value {
|
||||
@@ -135,18 +148,47 @@ impl CalendarObjectComponent {
|
||||
}
|
||||
Ok(Self::Journal(main_journal, overrides))
|
||||
}
|
||||
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self {
|
||||
Self::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_dtstart)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.min()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self {
|
||||
Self::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_last_occurence)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.max()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CalendarObject {
|
||||
data: CalendarObjectComponent,
|
||||
properties: Vec<Property>,
|
||||
id: String,
|
||||
ics: String,
|
||||
vtimezones: HashMap<String, IcalTimeZone>,
|
||||
}
|
||||
|
||||
impl CalendarObject {
|
||||
pub fn from_ics(ics: String) -> Result<Self, Error> {
|
||||
pub fn from_ics(ics: String, id: Option<String>) -> Result<Self, Error> {
|
||||
let mut parser = ical::IcalParser::new(BufReader::new(ics.as_bytes()));
|
||||
let cal = parser.next().ok_or(Error::MissingCalendar)??;
|
||||
if parser.next().is_some() {
|
||||
@@ -202,6 +244,7 @@ impl CalendarObject {
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
id: id.unwrap_or_else(|| data.get_uid().to_owned()),
|
||||
data,
|
||||
properties: cal.properties,
|
||||
ics,
|
||||
@@ -219,21 +262,20 @@ impl CalendarObject {
|
||||
&self.data
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_uid(&self) -> &str {
|
||||
self.data.get_uid()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_id(&self) -> &str {
|
||||
match &self.data {
|
||||
// We've made sure before that the first component exists and all components share the
|
||||
// same UID
|
||||
CalendarObjectComponent::Todo(todo, _) => todo.get_uid(),
|
||||
CalendarObjectComponent::Event(event, _) => event.event.get_uid(),
|
||||
CalendarObjectComponent::Journal(journal, _) => journal.get_uid(),
|
||||
}
|
||||
&self.id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_etag(&self) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(self.get_id());
|
||||
hasher.update(self.get_uid());
|
||||
hasher.update(self.get_ics());
|
||||
format!("\"{:x}\"", hasher.finalize())
|
||||
}
|
||||
@@ -254,31 +296,11 @@ impl CalendarObject {
|
||||
}
|
||||
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self.data {
|
||||
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_dtstart)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.min()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
self.data.get_first_occurence()
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
match &self.data {
|
||||
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
|
||||
.iter()
|
||||
.chain(std::iter::once(main_event))
|
||||
.map(super::event::EventObject::get_last_occurence)
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.max()),
|
||||
_ => Ok(None),
|
||||
}
|
||||
self.data.get_last_occurence()
|
||||
}
|
||||
|
||||
pub fn expand_recurrence(
|
||||
@@ -299,4 +321,11 @@ impl CalendarObject {
|
||||
_ => Ok(self.get_ics().to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_property(&self, name: &str) -> Option<&Property> {
|
||||
self.properties
|
||||
.iter()
|
||||
.find(|property| property.name == name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ END:VCALENDAR
|
||||
|
||||
#[test]
|
||||
fn parse_calendar_object() {
|
||||
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string()).unwrap();
|
||||
let object = CalendarObject::from_ics(MULTI_VEVENT.to_string(), None).unwrap();
|
||||
object.expand_recurrence(None, None).unwrap();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ reqwest.workspace = true
|
||||
thiserror.workspace = true
|
||||
async-trait.workspace = true
|
||||
axum.workspace = true
|
||||
tower-sessions = "0.14"
|
||||
tower-sessions.workspace = true
|
||||
axum-extra.workspace = true
|
||||
headers.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::synctoken::format_synctoken;
|
||||
use chrono::NaiveDateTime;
|
||||
use rustical_ical::CalendarObjectType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
pub struct CalendarMetadata {
|
||||
|
||||
@@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||
use chrono::NaiveDate;
|
||||
use rustical_ical::CalendarObject;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CalendarQuery {
|
||||
pub time_start: Option<NaiveDate>,
|
||||
pub time_end: Option<NaiveDate>,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::ChangeOperation;
|
||||
use crate::BEGIN_IMMEDIATE;
|
||||
use async_trait::async_trait;
|
||||
use derive_more::derive::Constructor;
|
||||
use rustical_ical::AddressObject;
|
||||
@@ -414,7 +415,11 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
addressbook_id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let addressbook =
|
||||
match Self::_get_addressbook(&mut *tx, principal, addressbook_id, use_trashbin).await {
|
||||
@@ -508,7 +513,11 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
object: AddressObject,
|
||||
overwrite: bool,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let object_id = object.get_id().to_owned();
|
||||
|
||||
@@ -554,7 +563,11 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
object_id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
Self::_delete_object(&mut *tx, principal, addressbook_id, object_id, use_trashbin).await?;
|
||||
|
||||
@@ -589,7 +602,11 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
addressbook_id: &str,
|
||||
object_id: &str,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
Self::_restore_object(&mut *tx, principal, addressbook_id, object_id).await?;
|
||||
|
||||
@@ -624,7 +641,11 @@ impl AddressbookStore for SqliteAddressbookStore {
|
||||
objects: Vec<AddressObject>,
|
||||
merge_existing: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let existing =
|
||||
match Self::_get_addressbook(&mut *tx, &addressbook.principal, &addressbook.id, true)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::ChangeOperation;
|
||||
use crate::BEGIN_IMMEDIATE;
|
||||
use async_trait::async_trait;
|
||||
use chrono::TimeDelta;
|
||||
use derive_more::derive::Constructor;
|
||||
@@ -16,19 +17,20 @@ use tracing::{error, instrument};
|
||||
struct CalendarObjectRow {
|
||||
id: String,
|
||||
ics: String,
|
||||
uid: String,
|
||||
}
|
||||
|
||||
impl TryFrom<CalendarObjectRow> for CalendarObject {
|
||||
type Error = rustical_store::Error;
|
||||
|
||||
fn try_from(value: CalendarObjectRow) -> Result<Self, Self::Error> {
|
||||
let object = Self::from_ics(value.ics)?;
|
||||
if object.get_id() != value.id {
|
||||
let object = Self::from_ics(value.ics, Some(value.id))?;
|
||||
if object.get_uid() != value.uid {
|
||||
return Err(rustical_store::Error::IcalError(
|
||||
rustical_ical::Error::InvalidData(format!(
|
||||
"object_id={} and UID={} don't match",
|
||||
object.get_id(),
|
||||
value.id
|
||||
"uid={} and UID={} don't match",
|
||||
value.uid,
|
||||
object.get_uid()
|
||||
)),
|
||||
));
|
||||
}
|
||||
@@ -280,7 +282,7 @@ impl SqliteCalendarStore {
|
||||
) -> Result<Vec<CalendarObject>, Error> {
|
||||
sqlx::query_as!(
|
||||
CalendarObjectRow,
|
||||
"SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
"SELECT id, uid, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
|
||||
principal,
|
||||
cal_id
|
||||
)
|
||||
@@ -305,7 +307,7 @@ impl SqliteCalendarStore {
|
||||
|
||||
sqlx::query_as!(
|
||||
CalendarObjectRow,
|
||||
r"SELECT id, ics FROM calendarobjects
|
||||
r"SELECT id, uid, ics FROM calendarobjects
|
||||
WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL
|
||||
AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))
|
||||
AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))
|
||||
@@ -334,7 +336,7 @@ impl SqliteCalendarStore {
|
||||
) -> Result<CalendarObject, Error> {
|
||||
sqlx::query_as!(
|
||||
CalendarObjectRow,
|
||||
"SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
"SELECT id, uid, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) OR ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
object_id,
|
||||
@@ -354,7 +356,7 @@ impl SqliteCalendarStore {
|
||||
object: CalendarObject,
|
||||
overwrite: bool,
|
||||
) -> Result<(), Error> {
|
||||
let (object_id, ics) = (object.get_id(), object.get_ics());
|
||||
let (object_id, uid, ics) = (object.get_id(), object.get_uid(), object.get_ics());
|
||||
|
||||
let first_occurence = object
|
||||
.get_first_occurence()
|
||||
@@ -373,10 +375,11 @@ impl SqliteCalendarStore {
|
||||
|
||||
(if overwrite {
|
||||
sqlx::query!(
|
||||
"REPLACE INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"REPLACE INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
object_id,
|
||||
uid,
|
||||
ics,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
@@ -386,10 +389,11 @@ impl SqliteCalendarStore {
|
||||
} else {
|
||||
// If the object already exists a database error is thrown and handled in error.rs
|
||||
sqlx::query!(
|
||||
"INSERT INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
"INSERT INTO calendarobjects (principal, cal_id, id, uid, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, ?, date(?), date(?), ?, ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
object_id,
|
||||
uid,
|
||||
ics,
|
||||
first_occurence,
|
||||
last_occurence,
|
||||
@@ -409,7 +413,7 @@ impl SqliteCalendarStore {
|
||||
executor: E,
|
||||
principal: &str,
|
||||
cal_id: &str,
|
||||
id: &str,
|
||||
object_id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
if use_trashbin {
|
||||
@@ -417,7 +421,7 @@ impl SqliteCalendarStore {
|
||||
"UPDATE calendarobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)",
|
||||
principal,
|
||||
cal_id,
|
||||
id
|
||||
object_id
|
||||
)
|
||||
.execute(executor)
|
||||
.await.map_err(crate::Error::from)?;
|
||||
@@ -425,7 +429,7 @@ impl SqliteCalendarStore {
|
||||
sqlx::query!(
|
||||
"DELETE FROM calendarobjects WHERE cal_id = ? AND id = ?",
|
||||
cal_id,
|
||||
id
|
||||
object_id
|
||||
)
|
||||
.execute(executor)
|
||||
.await
|
||||
@@ -539,7 +543,11 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let cal = match Self::_get_calendar(&mut *tx, principal, id, true).await {
|
||||
Ok(cal) => Some(cal),
|
||||
@@ -573,7 +581,11 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
objects: Vec<CalendarObject>,
|
||||
merge_existing: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let existing_cal =
|
||||
match Self::_get_calendar(&mut *tx, &calendar.principal, &calendar.id, true).await {
|
||||
@@ -663,7 +675,11 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
object: CalendarObject,
|
||||
overwrite: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
let object_id = object.get_id().to_owned();
|
||||
|
||||
@@ -713,7 +729,11 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
id: &str,
|
||||
use_trashbin: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
Self::_delete_object(&mut *tx, principal, cal_id, id, use_trashbin).await?;
|
||||
|
||||
@@ -737,7 +757,11 @@ impl CalendarStore for SqliteCalendarStore {
|
||||
cal_id: &str,
|
||||
object_id: &str,
|
||||
) -> Result<(), Error> {
|
||||
let mut tx = self.db.begin().await.map_err(crate::Error::from)?;
|
||||
let mut tx = self
|
||||
.db
|
||||
.begin_with(BEGIN_IMMEDIATE)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
|
||||
Self::_restore_object(&mut *tx, principal, cal_id, object_id).await?;
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ pub mod error;
|
||||
pub mod principal_store;
|
||||
pub mod subscription_store;
|
||||
|
||||
// Begin statement for write transactions
|
||||
pub const BEGIN_IMMEDIATE: &str = "BEGIN IMMEDIATE";
|
||||
|
||||
#[cfg(any(test, feature = "test"))]
|
||||
pub mod tests;
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@ impl Field {
|
||||
}
|
||||
}),
|
||||
(FieldType::Text, false) => Some(quote! {
|
||||
writer.write_event(Event::Text(BytesText::new(&self.#target_field_index)))?;
|
||||
writer.write_event(Event::Text(BytesText::new(self.#target_field_index.as_ref())))?;
|
||||
}),
|
||||
(FieldType::Tag, true) => {
|
||||
let field_name = self.xml_name();
|
||||
|
||||
Reference in New Issue
Block a user