Take the pain out of keeping all your calendars together

feat: add caldav support

There's quite a lot here - because caldav support needed some fair
refactoring/etc. to get working. Happily after this I can commit a
little more incrementally as I'll be able to deploy what I have here...

...due to needing a password, I don't have any caldav test data- the
format for making a dav subscription is something like so:

INSERT INTO dav_connections ("id", "url", "username", "password") VALUES ('00069420-74d2-4ba8-85d6-4fc679810c02', <base URI>, <username>, <password>);
INSERT INTO subscriptions ("id", "reference", "dav_connection") VALUES ('12a89516-65f7-4874-8449-671b7c8444af', '/my/href/of/my/calendar/', '00069420-74d2-4ba8-85d6-4fc679810c02');

It's worth noting that - I believe - we return some minorly noncompliant
data for colors (i.e. I think the list of values that are supported for
calendar colors is greater than the list of values supported for, say,
event colors). Happily, I guess, after I wrote the caldav colors feature
I found out that Google Calendar doesn't support ical colors anyway. I
also then didn't write the feature for direct ical subscriptions so...
...another time, maybe.

a.starrysky.fyi 4aa8bda2 7c4f8b3f

verified
+357 -9
Cargo.lock
··· 3 version = 4 4 5 [[package]] 6 name = "aho-corasick" 7 version = "1.1.3" 8 source = "registry+https://github.com/rust-lang/crates.io-index" 9 checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 dependencies = [ 11 "memchr", 12 ] 13 14 [[package]] ··· 45 checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 46 47 [[package]] 48 name = "async-lock" 49 version = "3.4.1" 50 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 124 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 125 126 [[package]] 127 name = "axum" 128 version = "0.8.6" 129 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 198 checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 199 200 [[package]] 201 name = "bitflags" 202 version = "2.10.0" 203 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 216 ] 217 218 [[package]] 219 name = "bumpalo" 220 version = "3.19.0" 221 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 240 "anyhow", 241 "axum", 242 "console_error_panic_hook", 243 "icalendar", 244 "leptos", 245 "leptos_axum", 246 "leptos_meta", 247 "leptos_router", 248 "reqwest", 249 "sqlx", 250 "tokio", 251 "uuid", ··· 265 checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" 266 dependencies = [ 267 "find-msvc-tools", 268 "shlex", 269 ] 270 271 [[package]] 272 name = "cfg-if" 273 version = "1.0.4" 274 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 304 ] 305 306 [[package]] 307 name = "codee" 308 version = "0.3.3" 309 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 321 checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" 322 323 [[package]] 324 name = "concurrent-queue" 325 version = "2.5.0" 326 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 413 version = "0.9.4" 414 source = "registry+https://github.com/rust-lang/crates.io-index" 415 checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 416 dependencies = [ 417 "core-foundation-sys", 418 "libc", ··· 449 checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 450 451 [[package]] 452 name = "crossbeam-queue" 453 version = "0.3.12" 454 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 560 checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" 561 562 [[package]] 563 name = "either" 564 version = "1.15.0" 565 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 642 ] 643 644 [[package]] 645 name = "fastrand" 646 version = "2.3.0" 647 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 654 checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 655 656 [[package]] 657 name = "flume" 658 version = "0.11.1" 659 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 699 dependencies = [ 700 "percent-encoding", 701 ] 702 703 [[package]] 704 name = "futures" ··· 839 ] 840 841 [[package]] 842 name = "gloo-net" 843 version = "0.6.0" 844 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1083 "http", 1084 "hyper", 1085 "hyper-util", 1086 "rustls", 1087 "rustls-pki-types", 1088 "tokio", 1089 "tokio-rustls", ··· 1166 "chrono", 1167 "chrono-tz", 1168 "iso8601", 1169 - "nom", 1170 "nom-language", 1171 "serde", 1172 "time", ··· 1327 source = "registry+https://github.com/rust-lang/crates.io-index" 1328 checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" 1329 dependencies = [ 1330 - "nom", 1331 ] 1332 1333 [[package]] ··· 1344 version = "1.0.15" 1345 source = "registry+https://github.com/rust-lang/crates.io-index" 1346 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1347 1348 [[package]] 1349 name = "js-sys" ··· 1501 "cfg-if", 1502 "convert_case 0.8.0", 1503 "html-escape", 1504 - "itertools", 1505 "leptos_hot_reload", 1506 "prettyplease", 1507 "proc-macro-error2", ··· 1593 checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 1594 1595 [[package]] 1596 name = "libm" 1597 version = "0.2.15" 1598 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1720 ] 1721 1722 [[package]] 1723 name = "mio" 1724 version = "1.1.0" 1725 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1759 "openssl-probe", 1760 "openssl-sys", 1761 "schannel", 1762 - "security-framework", 1763 "security-framework-sys", 1764 "tempfile", 1765 ] ··· 1772 1773 [[package]] 1774 name = "nom" 1775 version = "8.0.0" 1776 source = "registry+https://github.com/rust-lang/crates.io-index" 1777 checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" ··· 1785 source = "registry+https://github.com/rust-lang/crates.io-index" 1786 checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" 1787 dependencies = [ 1788 - "nom", 1789 ] 1790 1791 [[package]] ··· 2140 ] 2141 2142 [[package]] 2143 name = "quinn" 2144 version = "0.11.9" 2145 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2321 dependencies = [ 2322 "dashmap", 2323 "guardian", 2324 - "itertools", 2325 "or_poisoned", 2326 "paste", 2327 "reactive_graph", ··· 2508 source = "registry+https://github.com/rust-lang/crates.io-index" 2509 checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" 2510 dependencies = [ 2511 "once_cell", 2512 "ring", 2513 "rustls-pki-types", ··· 2517 ] 2518 2519 [[package]] 2520 name = "rustls-pki-types" 2521 version = "1.12.0" 2522 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2532 source = "registry+https://github.com/rust-lang/crates.io-index" 2533 checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" 2534 dependencies = [ 2535 "ring", 2536 "rustls-pki-types", 2537 "untrusted", ··· 2580 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2581 dependencies = [ 2582 "bitflags", 2583 - "core-foundation", 2584 "core-foundation-sys", 2585 "libc", 2586 "security-framework-sys", ··· 2799 "digest", 2800 "rand_core 0.6.4", 2801 ] 2802 2803 [[package]] 2804 name = "siphasher" ··· 3127 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3128 dependencies = [ 3129 "bitflags", 3130 - "core-foundation", 3131 "system-configuration-sys", 3132 ] 3133 ··· 3156 "futures", 3157 "html-escape", 3158 "indexmap", 3159 - "itertools", 3160 "js-sys", 3161 "linear-map", 3162 "next_tuple", ··· 4294 "quote", 4295 "syn", 4296 ]
··· 3 version = 4 4 5 [[package]] 6 + name = "adler2" 7 + version = "2.0.1" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 10 + 11 + [[package]] 12 name = "aho-corasick" 13 version = "1.1.3" 14 source = "registry+https://github.com/rust-lang/crates.io-index" 15 checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 dependencies = [ 17 "memchr", 18 + ] 19 + 20 + [[package]] 21 + name = "alloc-no-stdlib" 22 + version = "2.0.4" 23 + source = "registry+https://github.com/rust-lang/crates.io-index" 24 + checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" 25 + 26 + [[package]] 27 + name = "alloc-stdlib" 28 + version = "0.2.2" 29 + source = "registry+https://github.com/rust-lang/crates.io-index" 30 + checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" 31 + dependencies = [ 32 + "alloc-no-stdlib", 33 ] 34 35 [[package]] ··· 66 checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" 67 68 [[package]] 69 + name = "async-compression" 70 + version = "0.4.32" 71 + source = "registry+https://github.com/rust-lang/crates.io-index" 72 + checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" 73 + dependencies = [ 74 + "compression-codecs", 75 + "compression-core", 76 + "futures-core", 77 + "pin-project-lite", 78 + "tokio", 79 + ] 80 + 81 + [[package]] 82 name = "async-lock" 83 version = "3.4.1" 84 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 158 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 159 160 [[package]] 161 + name = "aws-lc-rs" 162 + version = "1.14.1" 163 + source = "registry+https://github.com/rust-lang/crates.io-index" 164 + checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" 165 + dependencies = [ 166 + "aws-lc-sys", 167 + "zeroize", 168 + ] 169 + 170 + [[package]] 171 + name = "aws-lc-sys" 172 + version = "0.32.3" 173 + source = "registry+https://github.com/rust-lang/crates.io-index" 174 + checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" 175 + dependencies = [ 176 + "bindgen", 177 + "cc", 178 + "cmake", 179 + "dunce", 180 + "fs_extra", 181 + ] 182 + 183 + [[package]] 184 name = "axum" 185 version = "0.8.6" 186 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 255 checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" 256 257 [[package]] 258 + name = "bindgen" 259 + version = "0.72.1" 260 + source = "registry+https://github.com/rust-lang/crates.io-index" 261 + checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" 262 + dependencies = [ 263 + "bitflags", 264 + "cexpr", 265 + "clang-sys", 266 + "itertools 0.13.0", 267 + "log", 268 + "prettyplease", 269 + "proc-macro2", 270 + "quote", 271 + "regex", 272 + "rustc-hash", 273 + "shlex", 274 + "syn", 275 + ] 276 + 277 + [[package]] 278 name = "bitflags" 279 version = "2.10.0" 280 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 293 ] 294 295 [[package]] 296 + name = "brotli" 297 + version = "8.0.2" 298 + source = "registry+https://github.com/rust-lang/crates.io-index" 299 + checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" 300 + dependencies = [ 301 + "alloc-no-stdlib", 302 + "alloc-stdlib", 303 + "brotli-decompressor", 304 + ] 305 + 306 + [[package]] 307 + name = "brotli-decompressor" 308 + version = "5.0.0" 309 + source = "registry+https://github.com/rust-lang/crates.io-index" 310 + checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" 311 + dependencies = [ 312 + "alloc-no-stdlib", 313 + "alloc-stdlib", 314 + ] 315 + 316 + [[package]] 317 name = "bumpalo" 318 version = "3.19.0" 319 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 338 "anyhow", 339 "axum", 340 "console_error_panic_hook", 341 + "fast-dav-rs", 342 "icalendar", 343 "leptos", 344 "leptos_axum", 345 "leptos_meta", 346 "leptos_router", 347 "reqwest", 348 + "rustls", 349 "sqlx", 350 "tokio", 351 "uuid", ··· 365 checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" 366 dependencies = [ 367 "find-msvc-tools", 368 + "jobserver", 369 + "libc", 370 "shlex", 371 ] 372 373 [[package]] 374 + name = "cexpr" 375 + version = "0.6.0" 376 + source = "registry+https://github.com/rust-lang/crates.io-index" 377 + checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 378 + dependencies = [ 379 + "nom 7.1.3", 380 + ] 381 + 382 + [[package]] 383 name = "cfg-if" 384 version = "1.0.4" 385 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 415 ] 416 417 [[package]] 418 + name = "clang-sys" 419 + version = "1.8.1" 420 + source = "registry+https://github.com/rust-lang/crates.io-index" 421 + checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 422 + dependencies = [ 423 + "glob", 424 + "libc", 425 + "libloading", 426 + ] 427 + 428 + [[package]] 429 + name = "cmake" 430 + version = "0.1.54" 431 + source = "registry+https://github.com/rust-lang/crates.io-index" 432 + checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" 433 + dependencies = [ 434 + "cc", 435 + ] 436 + 437 + [[package]] 438 name = "codee" 439 version = "0.3.3" 440 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 452 checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" 453 454 [[package]] 455 + name = "compression-codecs" 456 + version = "0.4.31" 457 + source = "registry+https://github.com/rust-lang/crates.io-index" 458 + checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" 459 + dependencies = [ 460 + "brotli", 461 + "compression-core", 462 + "flate2", 463 + "memchr", 464 + "zstd", 465 + "zstd-safe", 466 + ] 467 + 468 + [[package]] 469 + name = "compression-core" 470 + version = "0.4.29" 471 + source = "registry+https://github.com/rust-lang/crates.io-index" 472 + checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 473 + 474 + [[package]] 475 name = "concurrent-queue" 476 version = "2.5.0" 477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 564 version = "0.9.4" 565 source = "registry+https://github.com/rust-lang/crates.io-index" 566 checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 567 + dependencies = [ 568 + "core-foundation-sys", 569 + "libc", 570 + ] 571 + 572 + [[package]] 573 + name = "core-foundation" 574 + version = "0.10.1" 575 + source = "registry+https://github.com/rust-lang/crates.io-index" 576 + checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" 577 dependencies = [ 578 "core-foundation-sys", 579 "libc", ··· 610 checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 611 612 [[package]] 613 + name = "crc32fast" 614 + version = "1.5.0" 615 + source = "registry+https://github.com/rust-lang/crates.io-index" 616 + checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 617 + dependencies = [ 618 + "cfg-if", 619 + ] 620 + 621 + [[package]] 622 name = "crossbeam-queue" 623 version = "0.3.12" 624 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 730 checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408" 731 732 [[package]] 733 + name = "dunce" 734 + version = "1.0.5" 735 + source = "registry+https://github.com/rust-lang/crates.io-index" 736 + checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" 737 + 738 + [[package]] 739 name = "either" 740 version = "1.15.0" 741 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 818 ] 819 820 [[package]] 821 + name = "fast-dav-rs" 822 + version = "0.1.0" 823 + source = "registry+https://github.com/rust-lang/crates.io-index" 824 + checksum = "51f55d1a4f112e8df314be4b39dabee93c1d6d42a27af3cd0699be4315bc0cdf" 825 + dependencies = [ 826 + "anyhow", 827 + "async-compression", 828 + "base64", 829 + "bytes", 830 + "futures", 831 + "futures-util", 832 + "http-body-util", 833 + "hyper", 834 + "hyper-rustls", 835 + "hyper-util", 836 + "quick-xml", 837 + "tokio", 838 + "tokio-util", 839 + ] 840 + 841 + [[package]] 842 name = "fastrand" 843 version = "2.3.0" 844 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 851 checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" 852 853 [[package]] 854 + name = "flate2" 855 + version = "1.1.5" 856 + source = "registry+https://github.com/rust-lang/crates.io-index" 857 + checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" 858 + dependencies = [ 859 + "crc32fast", 860 + "miniz_oxide", 861 + ] 862 + 863 + [[package]] 864 name = "flume" 865 version = "0.11.1" 866 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 906 dependencies = [ 907 "percent-encoding", 908 ] 909 + 910 + [[package]] 911 + name = "fs_extra" 912 + version = "1.3.0" 913 + source = "registry+https://github.com/rust-lang/crates.io-index" 914 + checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 915 916 [[package]] 917 name = "futures" ··· 1052 ] 1053 1054 [[package]] 1055 + name = "glob" 1056 + version = "0.3.3" 1057 + source = "registry+https://github.com/rust-lang/crates.io-index" 1058 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 1059 + 1060 + [[package]] 1061 name = "gloo-net" 1062 version = "0.6.0" 1063 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1302 "http", 1303 "hyper", 1304 "hyper-util", 1305 + "log", 1306 "rustls", 1307 + "rustls-native-certs", 1308 "rustls-pki-types", 1309 "tokio", 1310 "tokio-rustls", ··· 1387 "chrono", 1388 "chrono-tz", 1389 "iso8601", 1390 + "nom 8.0.0", 1391 "nom-language", 1392 "serde", 1393 "time", ··· 1548 source = "registry+https://github.com/rust-lang/crates.io-index" 1549 checksum = "e1082f0c48f143442a1ac6122f67e360ceee130b967af4d50996e5154a45df46" 1550 dependencies = [ 1551 + "nom 8.0.0", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "itertools" 1556 + version = "0.13.0" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 1559 + dependencies = [ 1560 + "either", 1561 ] 1562 1563 [[package]] ··· 1574 version = "1.0.15" 1575 source = "registry+https://github.com/rust-lang/crates.io-index" 1576 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1577 + 1578 + [[package]] 1579 + name = "jobserver" 1580 + version = "0.1.34" 1581 + source = "registry+https://github.com/rust-lang/crates.io-index" 1582 + checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" 1583 + dependencies = [ 1584 + "getrandom 0.3.4", 1585 + "libc", 1586 + ] 1587 1588 [[package]] 1589 name = "js-sys" ··· 1741 "cfg-if", 1742 "convert_case 0.8.0", 1743 "html-escape", 1744 + "itertools 0.14.0", 1745 "leptos_hot_reload", 1746 "prettyplease", 1747 "proc-macro-error2", ··· 1833 checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 1834 1835 [[package]] 1836 + name = "libloading" 1837 + version = "0.8.9" 1838 + source = "registry+https://github.com/rust-lang/crates.io-index" 1839 + checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" 1840 + dependencies = [ 1841 + "cfg-if", 1842 + "windows-link 0.2.1", 1843 + ] 1844 + 1845 + [[package]] 1846 name = "libm" 1847 version = "0.2.15" 1848 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1970 ] 1971 1972 [[package]] 1973 + name = "minimal-lexical" 1974 + version = "0.2.1" 1975 + source = "registry+https://github.com/rust-lang/crates.io-index" 1976 + checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1977 + 1978 + [[package]] 1979 + name = "miniz_oxide" 1980 + version = "0.8.9" 1981 + source = "registry+https://github.com/rust-lang/crates.io-index" 1982 + checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1983 + dependencies = [ 1984 + "adler2", 1985 + "simd-adler32", 1986 + ] 1987 + 1988 + [[package]] 1989 name = "mio" 1990 version = "1.1.0" 1991 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2025 "openssl-probe", 2026 "openssl-sys", 2027 "schannel", 2028 + "security-framework 2.11.1", 2029 "security-framework-sys", 2030 "tempfile", 2031 ] ··· 2038 2039 [[package]] 2040 name = "nom" 2041 + version = "7.1.3" 2042 + source = "registry+https://github.com/rust-lang/crates.io-index" 2043 + checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 2044 + dependencies = [ 2045 + "memchr", 2046 + "minimal-lexical", 2047 + ] 2048 + 2049 + [[package]] 2050 + name = "nom" 2051 version = "8.0.0" 2052 source = "registry+https://github.com/rust-lang/crates.io-index" 2053 checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" ··· 2061 source = "registry+https://github.com/rust-lang/crates.io-index" 2062 checksum = "2de2bc5b451bfedaef92c90b8939a8fff5770bdcc1fafd6239d086aab8fa6b29" 2063 dependencies = [ 2064 + "nom 8.0.0", 2065 ] 2066 2067 [[package]] ··· 2416 ] 2417 2418 [[package]] 2419 + name = "quick-xml" 2420 + version = "0.38.3" 2421 + source = "registry+https://github.com/rust-lang/crates.io-index" 2422 + checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" 2423 + dependencies = [ 2424 + "memchr", 2425 + "tokio", 2426 + ] 2427 + 2428 + [[package]] 2429 name = "quinn" 2430 version = "0.11.9" 2431 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2607 dependencies = [ 2608 "dashmap", 2609 "guardian", 2610 + "itertools 0.14.0", 2611 "or_poisoned", 2612 "paste", 2613 "reactive_graph", ··· 2794 source = "registry+https://github.com/rust-lang/crates.io-index" 2795 checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" 2796 dependencies = [ 2797 + "aws-lc-rs", 2798 + "log", 2799 "once_cell", 2800 "ring", 2801 "rustls-pki-types", ··· 2805 ] 2806 2807 [[package]] 2808 + name = "rustls-native-certs" 2809 + version = "0.8.2" 2810 + source = "registry+https://github.com/rust-lang/crates.io-index" 2811 + checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" 2812 + dependencies = [ 2813 + "openssl-probe", 2814 + "rustls-pki-types", 2815 + "schannel", 2816 + "security-framework 3.5.1", 2817 + ] 2818 + 2819 + [[package]] 2820 name = "rustls-pki-types" 2821 version = "1.12.0" 2822 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2832 source = "registry+https://github.com/rust-lang/crates.io-index" 2833 checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" 2834 dependencies = [ 2835 + "aws-lc-rs", 2836 "ring", 2837 "rustls-pki-types", 2838 "untrusted", ··· 2881 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 2882 dependencies = [ 2883 "bitflags", 2884 + "core-foundation 0.9.4", 2885 + "core-foundation-sys", 2886 + "libc", 2887 + "security-framework-sys", 2888 + ] 2889 + 2890 + [[package]] 2891 + name = "security-framework" 2892 + version = "3.5.1" 2893 + source = "registry+https://github.com/rust-lang/crates.io-index" 2894 + checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" 2895 + dependencies = [ 2896 + "bitflags", 2897 + "core-foundation 0.10.1", 2898 "core-foundation-sys", 2899 "libc", 2900 "security-framework-sys", ··· 3113 "digest", 3114 "rand_core 0.6.4", 3115 ] 3116 + 3117 + [[package]] 3118 + name = "simd-adler32" 3119 + version = "0.3.7" 3120 + source = "registry+https://github.com/rust-lang/crates.io-index" 3121 + checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 3122 3123 [[package]] 3124 name = "siphasher" ··· 3447 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 3448 dependencies = [ 3449 "bitflags", 3450 + "core-foundation 0.9.4", 3451 "system-configuration-sys", 3452 ] 3453 ··· 3476 "futures", 3477 "html-escape", 3478 "indexmap", 3479 + "itertools 0.14.0", 3480 "js-sys", 3481 "linear-map", 3482 "next_tuple", ··· 4614 "quote", 4615 "syn", 4616 ] 4617 + 4618 + [[package]] 4619 + name = "zstd" 4620 + version = "0.13.3" 4621 + source = "registry+https://github.com/rust-lang/crates.io-index" 4622 + checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 4623 + dependencies = [ 4624 + "zstd-safe", 4625 + ] 4626 + 4627 + [[package]] 4628 + name = "zstd-safe" 4629 + version = "7.2.4" 4630 + source = "registry+https://github.com/rust-lang/crates.io-index" 4631 + checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 4632 + dependencies = [ 4633 + "zstd-sys", 4634 + ] 4635 + 4636 + [[package]] 4637 + name = "zstd-sys" 4638 + version = "2.0.16+zstd.1.5.7" 4639 + source = "registry+https://github.com/rust-lang/crates.io-index" 4640 + checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" 4641 + dependencies = [ 4642 + "cc", 4643 + "pkg-config", 4644 + ]
+4
Cargo.toml
··· 20 anyhow = "1.0.100" 21 reqwest = { version = "0.12.24", features = ["rustls-tls-webpki-roots", "charset", "http2"], optional = true } 22 icalendar = { version = "0.17.5", features = ["parser", "serde", "chrono-tz", "time",], optional = true } 23 24 [features] 25 hydrate = [ ··· 29 ] 30 ssr = [ 31 "dep:axum", 32 "dep:icalendar", 33 "dep:leptos_axum", 34 "dep:reqwest", 35 "dep:sqlx", 36 "dep:tokio", 37 "dep:uuid",
··· 20 anyhow = "1.0.100" 21 reqwest = { version = "0.12.24", features = ["rustls-tls-webpki-roots", "charset", "http2"], optional = true } 22 icalendar = { version = "0.17.5", features = ["parser", "serde", "chrono-tz", "time",], optional = true } 23 + fast-dav-rs = { version = "0.1.0", optional = true } 24 + rustls = { version = "0.23.34", features = ["ring"], optional = true } 25 26 [features] 27 hydrate = [ ··· 31 ] 32 ssr = [ 33 "dep:axum", 34 + "dep:fast-dav-rs", 35 "dep:icalendar", 36 "dep:leptos_axum", 37 "dep:reqwest", 38 + "dep:rustls", 39 "dep:sqlx", 40 "dep:tokio", 41 "dep:uuid",
+11
migrations/20251027071639_add-caldav.sql
···
··· 1 + ALTER TABLE "subscriptions" RENAME COLUMN "url" TO "reference"; 2 + ALTER TABLE "subscriptions" ADD COLUMN "dav_connection" uuid; 3 + 4 + CREATE TABLE "dav_connections" ( 5 + "id" uuid PRIMARY KEY, 6 + "url" varchar NOT NULL, 7 + "username" varchar, 8 + "password" varchar 9 + ); 10 + 11 + ALTER TABLE "subscriptions" ADD CONSTRAINT "subscription_dav_connection" FOREIGN KEY ("dav_connection") REFERENCES "dav_connections" ("id");
+112 -18
src/calendar.rs
··· 1 use std::ops::DerefMut; 2 - 3 use axum::extract::Path; 4 use leptos::prelude::ServerFnError; 5 use leptos::server; 6 use tokio::task::JoinSet; ··· 16 17 #[server] 18 async fn events_by_calendar(id: Uuid) -> anyhow::Result<String, ServerFnError> { 19 - let state = STATE.get().unwrap(); 20 - 21 - println!("{:?}", id); 22 23 - let subscription_urls = sqlx::query!( 24 - "SELECT url FROM subscriptions 25 JOIN calendars_subscriptions on subscriptions.id = calendars_subscriptions.subscriptions_id 26 JOIN calendars on calendars.id = calendars_subscriptions.calendars_id 27 - WHERE calendars.id = $1", 28 id 29 ).fetch_all(state.sqlx_connection.lock().await.deref_mut()).await?; 30 31 - println!("{:#?}", subscription_urls); 32 - 33 let mut requests = JoinSet::new(); 34 35 - for url in subscription_urls { 36 requests.spawn((async || { 37 - let result = state.reqwest_client.get(url.url).send().await; 38 39 let Ok(response) = result else { 40 return None 41 }; 42 43 - response.text().await.ok() 44 })()); 45 } 46 47 - let results = requests.join_all().await; 48 - let icals = results.iter().filter_map(|ical| ical.to_owned()); 49 - 50 - let calendars: Vec<icalendar::Calendar> = icals.map(|ical| ical.parse::<icalendar::Calendar>()).filter_map(|calendar| calendar.ok()).collect(); 51 52 let mut merged = icalendar::Calendar::new(); 53 54 - for calendar in calendars { 55 - merged.extend(calendar.components); 56 } 57 58 Ok(merged.to_string()) 59 }
··· 1 use std::ops::DerefMut; 2 use axum::extract::Path; 3 + use fast_dav_rs::CalDavClient; 4 + use icalendar::Component; 5 use leptos::prelude::ServerFnError; 6 use leptos::server; 7 use tokio::task::JoinSet; ··· 17 18 #[server] 19 async fn events_by_calendar(id: Uuid) -> anyhow::Result<String, ServerFnError> { 20 + let state = STATE.get().expect("Failed to get STATE - is it set yet?"); 21 22 + let subscriptions = sqlx::query!( 23 + r#"SELECT subscriptions.reference, dav_connections.url as "dav_connection_url?", dav_connections.username, dav_connections.password FROM subscriptions 24 JOIN calendars_subscriptions on subscriptions.id = calendars_subscriptions.subscriptions_id 25 JOIN calendars on calendars.id = calendars_subscriptions.calendars_id 26 + LEFT JOIN dav_connections on subscriptions.dav_connection = dav_connections.id 27 + WHERE calendars.id = $1"#, 28 id 29 ).fetch_all(state.sqlx_connection.lock().await.deref_mut()).await?; 30 31 let mut requests = JoinSet::new(); 32 33 + for subscription in subscriptions { 34 requests.spawn((async || { 35 + if let (Some(dav_connection_url), username, password) = (subscription.dav_connection_url, subscription.username, subscription.password) { 36 + // fast_dav_rs client doesn't follow redirects, so we must use reqwest to check first so, e.g., stalwart .well-known/caldav URLs work 37 + let base_url = state.reqwest_client.head(&dav_connection_url).send().await 38 + .and_then(|response| Ok(response.url().to_string())) 39 + .unwrap_or_else(|_| dav_connection_url); 40 + 41 + let Ok(client) = CalDavClient::new(&base_url, username.as_deref(), password.as_deref()) else { 42 + return None 43 + }; 44 + 45 + return get_dav_calendar(client, &subscription.reference).await 46 + } 47 + let result = state.reqwest_client.get(subscription.reference).send().await; 48 49 let Ok(response) = result else { 50 return None 51 }; 52 53 + let Ok(text) = response.text().await else { 54 + return None 55 + }; 56 + 57 + text.parse::<icalendar::Calendar>().ok() 58 })()); 59 } 60 61 + let calendars = requests.join_all().await; 62 63 let mut merged = icalendar::Calendar::new(); 64 65 + for maybe_calendar in calendars { 66 + if let Some(calendar) = maybe_calendar { 67 + merged.extend(calendar.components); 68 + } 69 } 70 71 Ok(merged.to_string()) 72 } 73 + 74 + #[cfg(feature = "ssr")] 75 + async fn list_dav_calendars(client: &CalDavClient) -> anyhow::Result<Vec<fast_dav_rs::CalendarInfo>, anyhow::Error> { 76 + let Some(principal) = client.discover_current_user_principal().await? else { 77 + return Err(anyhow::anyhow!("Incompatible: CalDav server does not indicate principal")) 78 + }; 79 + 80 + let home_sets = client.discover_calendar_home_set(&principal).await?; 81 + 82 + let mut calendars = vec![]; 83 + 84 + for home_set in home_sets { 85 + calendars.extend(client.list_calendars(&home_set).await?); 86 + } 87 + 88 + Ok(calendars) 89 + } 90 + 91 + fn get_colored_components(color: Option<&str>, calendar: icalendar::Calendar) -> Vec<icalendar::CalendarComponent> { 92 + let Some(color) = calendar.property_value("COLOR").or_else(|| color) else { 93 + return calendar.components 94 + }; 95 + 96 + return calendar.components.iter().map(ToOwned::to_owned).map(|component| { 97 + match component { 98 + icalendar::CalendarComponent::Event(mut event) => { 99 + if event.property_value("COLOR").is_none() { 100 + event.add_property("COLOR", color); 101 + } 102 + 103 + icalendar::CalendarComponent::Event(event) 104 + }, 105 + icalendar::CalendarComponent::Todo(mut todo) => { 106 + if todo.property_value("COLOR").is_none() { 107 + todo.add_property("COLOR", color); 108 + } 109 + 110 + icalendar::CalendarComponent::Todo(todo) 111 + }, 112 + icalendar::CalendarComponent::Venue(mut venue) => { 113 + if venue.property_value("COLOR").is_none() { 114 + venue.add_property("COLOR", color); 115 + } 116 + 117 + icalendar::CalendarComponent::Venue(venue) 118 + }, 119 + other => other 120 + } 121 + }).collect() 122 + } 123 + 124 + #[cfg(feature = "ssr")] 125 + async fn get_dav_calendar(client: fast_dav_rs::CalDavClient, reference: &str) -> Option<icalendar::Calendar> { 126 + let all_calendars = list_dav_calendars(&client).await.unwrap_or_else(|_| vec![]); 127 + 128 + let color = all_calendars 129 + .iter() 130 + .filter(|calendar| calendar.href == reference && calendar.color.is_some()) 131 + .next() 132 + .and_then(|calendar| calendar.color.clone()); 133 + 134 + let Ok(result) = client.calendar_query_timerange(reference, "VEVENT", None, None, true).await else { 135 + return None 136 + }; 137 + 138 + let mut calendar = icalendar::Calendar::new(); 139 + 140 + for calendar_object in result { 141 + let Some(calendar_data) = calendar_object.calendar_data else { 142 + continue 143 + }; 144 + 145 + let Ok(parsed) = calendar_data.parse::<icalendar::Calendar>() else { 146 + continue 147 + }; 148 + 149 + calendar.extend(get_colored_components(color.as_deref(), parsed)) 150 + } 151 + 152 + Some(calendar) 153 + }
+5 -3
src/main.rs
··· 25 use leptos_axum::{generate_route_list, LeptosRoutes}; 26 use calpoll::app::*; 27 use sqlx::Connection; 28 29 let connection = 30 PgConnection::connect( 31 env::var("DATABASE_URL") 32 .expect("Please ensure you set your database URL in the $DATABASE_URL environment variable") 33 - .as_str()).await.unwrap(); 34 35 STATE.set(State { 36 sqlx_connection: Mutex::new(connection), 37 reqwest_client: reqwest::Client::new(), 38 - }).unwrap(); 39 40 let conf = get_configuration(None).unwrap(); 41 let addr = conf.leptos_options.site_addr; ··· 58 let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 59 axum::serve(listener, app.into_make_service()) 60 .await 61 - .unwrap(); 62 }
··· 25 use leptos_axum::{generate_route_list, LeptosRoutes}; 26 use calpoll::app::*; 27 use sqlx::Connection; 28 + 29 + rustls::crypto::ring::default_provider().install_default().expect("Failed to set default crypto provider for rustls..."); // Needed for fast-dav-rs since as it doesn't properly enable features... 30 31 let connection = 32 PgConnection::connect( 33 env::var("DATABASE_URL") 34 .expect("Please ensure you set your database URL in the $DATABASE_URL environment variable") 35 + .as_str()).await.expect("Failed to connect to database defined in $DATABASE_URL"); 36 37 STATE.set(State { 38 sqlx_connection: Mutex::new(connection), 39 reqwest_client: reqwest::Client::new(), 40 + }).expect("Consistency issue: failed to set STATE - was it already set?"); 41 42 let conf = get_configuration(None).unwrap(); 43 let addr = conf.leptos_options.site_addr; ··· 60 let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); 61 axum::serve(listener, app.into_make_service()) 62 .await 63 + .expect("Failed to start axum server"); 64 }
+2 -2
test-data/merge-holidays-moon-phases.sql
··· 2 3 INSERT INTO calendars ("id", "user") VALUES ('32da8949-b21c-469d-a2bc-f5763af186c6', '11cd80d6-cf3d-4d93-9d47-4a383eaf1a10'); 4 5 - INSERT INTO subscriptions ("id", "url") VALUES ('797de795-0ae5-443c-91d1-1d68f9d44889', 'https://calendar.google.com/calendar/ical/en.uk%23holiday%40group.v.calendar.google.com/public/basic.ics'); 6 - INSERT INTO subscriptions ("id", "url") VALUES ('53d15eed-9db4-40ac-a5b1-b6df114847b2', 'https://calendar.google.com/calendar/ical/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic.ics'); 7 8 INSERT INTO calendars_subscriptions ("calendars_id", "subscriptions_id") VALUES ('32da8949-b21c-469d-a2bc-f5763af186c6', '797de795-0ae5-443c-91d1-1d68f9d44889'); 9 INSERT INTO calendars_subscriptions ("calendars_id", "subscriptions_id") VALUES ('32da8949-b21c-469d-a2bc-f5763af186c6', '53d15eed-9db4-40ac-a5b1-b6df114847b2');
··· 2 3 INSERT INTO calendars ("id", "user") VALUES ('32da8949-b21c-469d-a2bc-f5763af186c6', '11cd80d6-cf3d-4d93-9d47-4a383eaf1a10'); 4 5 + INSERT INTO subscriptions ("id", "reference") VALUES ('797de795-0ae5-443c-91d1-1d68f9d44889', 'https://calendar.google.com/calendar/ical/en.uk%23holiday%40group.v.calendar.google.com/public/basic.ics'); 6 + INSERT INTO subscriptions ("id", "reference") VALUES ('53d15eed-9db4-40ac-a5b1-b6df114847b2', 'https://calendar.google.com/calendar/ical/ht3jlfaac5lfd6263ulfh4tql8%40group.calendar.google.com/public/basic.ics'); 7 8 INSERT INTO calendars_subscriptions ("calendars_id", "subscriptions_id") VALUES ('32da8949-b21c-469d-a2bc-f5763af186c6', '797de795-0ae5-443c-91d1-1d68f9d44889'); 9 INSERT INTO calendars_subscriptions ("calendars_id", "subscriptions_id") VALUES ('32da8949-b21c-469d-a2bc-f5763af186c6', '53d15eed-9db4-40ac-a5b1-b6df114847b2');