personal activity index (bluesky, leaflet, substack) pai.desertthunder.dev
rss bluesky

feat: add support for Bluesky and Leaflet sources, including fetchers and configuration

* added configuration structures for Substack, Bluesky, and Leaflet sources

* enhanced error handling and output formatting using owo-colors.

+3359 -70
+2 -1
.gitignore
··· 18 18 # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 19 19 # and can be added to the global gitignore or merged into this file. For a more nuclear 20 20 # option (not recommended) you can uncomment the following to ignore the entire idea folder. 21 - #.idea/ 21 + .idea/ 22 + .vscode/
+1456 -5
Cargo.lock
··· 3 3 version = 4 4 4 5 5 [[package]] 6 + name = "aho-corasick" 7 + version = "1.1.4" 8 + source = "registry+https://github.com/rust-lang/crates.io-index" 9 + checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" 10 + dependencies = [ 11 + "memchr", 12 + ] 13 + 14 + [[package]] 6 15 name = "android_system_properties" 7 16 version = "0.1.5" 8 17 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 47 56 source = "registry+https://github.com/rust-lang/crates.io-index" 48 57 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" 49 58 dependencies = [ 50 - "windows-sys", 59 + "windows-sys 0.61.2", 51 60 ] 52 61 53 62 [[package]] ··· 58 67 dependencies = [ 59 68 "anstyle", 60 69 "once_cell_polyfill", 61 - "windows-sys", 70 + "windows-sys 0.61.2", 62 71 ] 63 72 64 73 [[package]] 74 + name = "atomic-waker" 75 + version = "1.1.2" 76 + source = "registry+https://github.com/rust-lang/crates.io-index" 77 + checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 78 + 79 + [[package]] 65 80 name = "autocfg" 66 81 version = "1.5.0" 67 82 source = "registry+https://github.com/rust-lang/crates.io-index" 68 83 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 69 84 70 85 [[package]] 86 + name = "base64" 87 + version = "0.22.1" 88 + source = "registry+https://github.com/rust-lang/crates.io-index" 89 + checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 90 + 91 + [[package]] 71 92 name = "bitflags" 72 93 version = "2.10.0" 73 94 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 78 99 version = "3.19.0" 79 100 source = "registry+https://github.com/rust-lang/crates.io-index" 80 101 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" 102 + 103 + [[package]] 104 + name = "bytes" 105 + version = "1.11.0" 106 + source = "registry+https://github.com/rust-lang/crates.io-index" 107 + checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" 81 108 82 109 [[package]] 83 110 name = "cc" ··· 104 131 "iana-time-zone", 105 132 "js-sys", 106 133 "num-traits", 134 + "serde", 107 135 "wasm-bindgen", 108 136 "windows-link", 109 137 ] ··· 155 183 checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 156 184 157 185 [[package]] 186 + name = "core-foundation" 187 + version = "0.9.4" 188 + source = "registry+https://github.com/rust-lang/crates.io-index" 189 + checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 190 + dependencies = [ 191 + "core-foundation-sys", 192 + "libc", 193 + ] 194 + 195 + [[package]] 158 196 name = "core-foundation-sys" 159 197 version = "0.8.7" 160 198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 178 216 "libc", 179 217 "option-ext", 180 218 "redox_users", 181 - "windows-sys", 219 + "windows-sys 0.61.2", 220 + ] 221 + 222 + [[package]] 223 + name = "displaydoc" 224 + version = "0.2.5" 225 + source = "registry+https://github.com/rust-lang/crates.io-index" 226 + checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 227 + dependencies = [ 228 + "proc-macro2", 229 + "quote", 230 + "syn", 231 + ] 232 + 233 + [[package]] 234 + name = "encoding_rs" 235 + version = "0.8.35" 236 + source = "registry+https://github.com/rust-lang/crates.io-index" 237 + checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 238 + dependencies = [ 239 + "cfg-if", 240 + ] 241 + 242 + [[package]] 243 + name = "equivalent" 244 + version = "1.0.2" 245 + source = "registry+https://github.com/rust-lang/crates.io-index" 246 + checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 247 + 248 + [[package]] 249 + name = "errno" 250 + version = "0.3.14" 251 + source = "registry+https://github.com/rust-lang/crates.io-index" 252 + checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 253 + dependencies = [ 254 + "libc", 255 + "windows-sys 0.61.2", 182 256 ] 183 257 184 258 [[package]] ··· 194 268 checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 195 269 196 270 [[package]] 271 + name = "fastrand" 272 + version = "2.3.0" 273 + source = "registry+https://github.com/rust-lang/crates.io-index" 274 + checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 275 + 276 + [[package]] 277 + name = "feed-rs" 278 + version = "2.3.1" 279 + source = "registry+https://github.com/rust-lang/crates.io-index" 280 + checksum = "e4c0591d23efd0d595099af69a31863ac1823046b1b021e3b06ba3aae7e00991" 281 + dependencies = [ 282 + "chrono", 283 + "mediatype", 284 + "quick-xml", 285 + "regex", 286 + "serde", 287 + "serde_json", 288 + "siphasher", 289 + "url", 290 + "uuid", 291 + ] 292 + 293 + [[package]] 197 294 name = "find-msvc-tools" 198 295 version = "0.1.5" 199 296 source = "registry+https://github.com/rust-lang/crates.io-index" 200 297 checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" 201 298 202 299 [[package]] 300 + name = "fnv" 301 + version = "1.0.7" 302 + source = "registry+https://github.com/rust-lang/crates.io-index" 303 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 304 + 305 + [[package]] 203 306 name = "foldhash" 204 307 version = "0.1.5" 205 308 source = "registry+https://github.com/rust-lang/crates.io-index" 206 309 checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 207 310 208 311 [[package]] 312 + name = "foreign-types" 313 + version = "0.3.2" 314 + source = "registry+https://github.com/rust-lang/crates.io-index" 315 + checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 316 + dependencies = [ 317 + "foreign-types-shared", 318 + ] 319 + 320 + [[package]] 321 + name = "foreign-types-shared" 322 + version = "0.1.1" 323 + source = "registry+https://github.com/rust-lang/crates.io-index" 324 + checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 325 + 326 + [[package]] 327 + name = "form_urlencoded" 328 + version = "1.2.2" 329 + source = "registry+https://github.com/rust-lang/crates.io-index" 330 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 331 + dependencies = [ 332 + "percent-encoding", 333 + ] 334 + 335 + [[package]] 336 + name = "futures-channel" 337 + version = "0.3.31" 338 + source = "registry+https://github.com/rust-lang/crates.io-index" 339 + checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 340 + dependencies = [ 341 + "futures-core", 342 + ] 343 + 344 + [[package]] 345 + name = "futures-core" 346 + version = "0.3.31" 347 + source = "registry+https://github.com/rust-lang/crates.io-index" 348 + checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 349 + 350 + [[package]] 351 + name = "futures-sink" 352 + version = "0.3.31" 353 + source = "registry+https://github.com/rust-lang/crates.io-index" 354 + checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 355 + 356 + [[package]] 357 + name = "futures-task" 358 + version = "0.3.31" 359 + source = "registry+https://github.com/rust-lang/crates.io-index" 360 + checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 361 + 362 + [[package]] 363 + name = "futures-util" 364 + version = "0.3.31" 365 + source = "registry+https://github.com/rust-lang/crates.io-index" 366 + checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 367 + dependencies = [ 368 + "futures-core", 369 + "futures-task", 370 + "pin-project-lite", 371 + "pin-utils", 372 + ] 373 + 374 + [[package]] 209 375 name = "getrandom" 210 376 version = "0.2.16" 211 377 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 217 383 ] 218 384 219 385 [[package]] 386 + name = "getrandom" 387 + version = "0.3.4" 388 + source = "registry+https://github.com/rust-lang/crates.io-index" 389 + checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" 390 + dependencies = [ 391 + "cfg-if", 392 + "libc", 393 + "r-efi", 394 + "wasip2", 395 + ] 396 + 397 + [[package]] 398 + name = "h2" 399 + version = "0.4.12" 400 + source = "registry+https://github.com/rust-lang/crates.io-index" 401 + checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" 402 + dependencies = [ 403 + "atomic-waker", 404 + "bytes", 405 + "fnv", 406 + "futures-core", 407 + "futures-sink", 408 + "http", 409 + "indexmap", 410 + "slab", 411 + "tokio", 412 + "tokio-util", 413 + "tracing", 414 + ] 415 + 416 + [[package]] 220 417 name = "hashbrown" 221 418 version = "0.15.5" 222 419 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 226 423 ] 227 424 228 425 [[package]] 426 + name = "hashbrown" 427 + version = "0.16.1" 428 + source = "registry+https://github.com/rust-lang/crates.io-index" 429 + checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" 430 + 431 + [[package]] 229 432 name = "hashlink" 230 433 version = "0.10.0" 231 434 source = "registry+https://github.com/rust-lang/crates.io-index" 232 435 checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 233 436 dependencies = [ 234 - "hashbrown", 437 + "hashbrown 0.15.5", 235 438 ] 236 439 237 440 [[package]] ··· 241 444 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 242 445 243 446 [[package]] 447 + name = "http" 448 + version = "1.3.1" 449 + source = "registry+https://github.com/rust-lang/crates.io-index" 450 + checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 451 + dependencies = [ 452 + "bytes", 453 + "fnv", 454 + "itoa", 455 + ] 456 + 457 + [[package]] 458 + name = "http-body" 459 + version = "1.0.1" 460 + source = "registry+https://github.com/rust-lang/crates.io-index" 461 + checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 462 + dependencies = [ 463 + "bytes", 464 + "http", 465 + ] 466 + 467 + [[package]] 468 + name = "http-body-util" 469 + version = "0.1.3" 470 + source = "registry+https://github.com/rust-lang/crates.io-index" 471 + checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 472 + dependencies = [ 473 + "bytes", 474 + "futures-core", 475 + "http", 476 + "http-body", 477 + "pin-project-lite", 478 + ] 479 + 480 + [[package]] 481 + name = "httparse" 482 + version = "1.10.1" 483 + source = "registry+https://github.com/rust-lang/crates.io-index" 484 + checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 485 + 486 + [[package]] 487 + name = "hyper" 488 + version = "1.8.1" 489 + source = "registry+https://github.com/rust-lang/crates.io-index" 490 + checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" 491 + dependencies = [ 492 + "atomic-waker", 493 + "bytes", 494 + "futures-channel", 495 + "futures-core", 496 + "h2", 497 + "http", 498 + "http-body", 499 + "httparse", 500 + "itoa", 501 + "pin-project-lite", 502 + "pin-utils", 503 + "smallvec", 504 + "tokio", 505 + "want", 506 + ] 507 + 508 + [[package]] 509 + name = "hyper-rustls" 510 + version = "0.27.7" 511 + source = "registry+https://github.com/rust-lang/crates.io-index" 512 + checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" 513 + dependencies = [ 514 + "http", 515 + "hyper", 516 + "hyper-util", 517 + "rustls", 518 + "rustls-pki-types", 519 + "tokio", 520 + "tokio-rustls", 521 + "tower-service", 522 + ] 523 + 524 + [[package]] 525 + name = "hyper-tls" 526 + version = "0.6.0" 527 + source = "registry+https://github.com/rust-lang/crates.io-index" 528 + checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 529 + dependencies = [ 530 + "bytes", 531 + "http-body-util", 532 + "hyper", 533 + "hyper-util", 534 + "native-tls", 535 + "tokio", 536 + "tokio-native-tls", 537 + "tower-service", 538 + ] 539 + 540 + [[package]] 541 + name = "hyper-util" 542 + version = "0.1.18" 543 + source = "registry+https://github.com/rust-lang/crates.io-index" 544 + checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" 545 + dependencies = [ 546 + "base64", 547 + "bytes", 548 + "futures-channel", 549 + "futures-core", 550 + "futures-util", 551 + "http", 552 + "http-body", 553 + "hyper", 554 + "ipnet", 555 + "libc", 556 + "percent-encoding", 557 + "pin-project-lite", 558 + "socket2", 559 + "system-configuration", 560 + "tokio", 561 + "tower-service", 562 + "tracing", 563 + "windows-registry", 564 + ] 565 + 566 + [[package]] 244 567 name = "iana-time-zone" 245 568 version = "0.1.64" 246 569 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 265 588 ] 266 589 267 590 [[package]] 591 + name = "icu_collections" 592 + version = "2.1.1" 593 + source = "registry+https://github.com/rust-lang/crates.io-index" 594 + checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" 595 + dependencies = [ 596 + "displaydoc", 597 + "potential_utf", 598 + "yoke", 599 + "zerofrom", 600 + "zerovec", 601 + ] 602 + 603 + [[package]] 604 + name = "icu_locale_core" 605 + version = "2.1.1" 606 + source = "registry+https://github.com/rust-lang/crates.io-index" 607 + checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" 608 + dependencies = [ 609 + "displaydoc", 610 + "litemap", 611 + "tinystr", 612 + "writeable", 613 + "zerovec", 614 + ] 615 + 616 + [[package]] 617 + name = "icu_normalizer" 618 + version = "2.1.1" 619 + source = "registry+https://github.com/rust-lang/crates.io-index" 620 + checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" 621 + dependencies = [ 622 + "icu_collections", 623 + "icu_normalizer_data", 624 + "icu_properties", 625 + "icu_provider", 626 + "smallvec", 627 + "zerovec", 628 + ] 629 + 630 + [[package]] 631 + name = "icu_normalizer_data" 632 + version = "2.1.1" 633 + source = "registry+https://github.com/rust-lang/crates.io-index" 634 + checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" 635 + 636 + [[package]] 637 + name = "icu_properties" 638 + version = "2.1.1" 639 + source = "registry+https://github.com/rust-lang/crates.io-index" 640 + checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" 641 + dependencies = [ 642 + "icu_collections", 643 + "icu_locale_core", 644 + "icu_properties_data", 645 + "icu_provider", 646 + "zerotrie", 647 + "zerovec", 648 + ] 649 + 650 + [[package]] 651 + name = "icu_properties_data" 652 + version = "2.1.1" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" 655 + 656 + [[package]] 657 + name = "icu_provider" 658 + version = "2.1.1" 659 + source = "registry+https://github.com/rust-lang/crates.io-index" 660 + checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" 661 + dependencies = [ 662 + "displaydoc", 663 + "icu_locale_core", 664 + "writeable", 665 + "yoke", 666 + "zerofrom", 667 + "zerotrie", 668 + "zerovec", 669 + ] 670 + 671 + [[package]] 672 + name = "idna" 673 + version = "1.1.0" 674 + source = "registry+https://github.com/rust-lang/crates.io-index" 675 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 676 + dependencies = [ 677 + "idna_adapter", 678 + "smallvec", 679 + "utf8_iter", 680 + ] 681 + 682 + [[package]] 683 + name = "idna_adapter" 684 + version = "1.2.1" 685 + source = "registry+https://github.com/rust-lang/crates.io-index" 686 + checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 687 + dependencies = [ 688 + "icu_normalizer", 689 + "icu_properties", 690 + ] 691 + 692 + [[package]] 693 + name = "indexmap" 694 + version = "2.12.1" 695 + source = "registry+https://github.com/rust-lang/crates.io-index" 696 + checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" 697 + dependencies = [ 698 + "equivalent", 699 + "hashbrown 0.16.1", 700 + ] 701 + 702 + [[package]] 703 + name = "ipnet" 704 + version = "2.11.0" 705 + source = "registry+https://github.com/rust-lang/crates.io-index" 706 + checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 707 + 708 + [[package]] 709 + name = "iri-string" 710 + version = "0.7.9" 711 + source = "registry+https://github.com/rust-lang/crates.io-index" 712 + checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" 713 + dependencies = [ 714 + "memchr", 715 + "serde", 716 + ] 717 + 718 + [[package]] 268 719 name = "is_terminal_polyfill" 269 720 version = "1.70.2" 270 721 source = "registry+https://github.com/rust-lang/crates.io-index" 271 722 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 723 + 724 + [[package]] 725 + name = "itoa" 726 + version = "1.0.15" 727 + source = "registry+https://github.com/rust-lang/crates.io-index" 728 + checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 272 729 273 730 [[package]] 274 731 name = "js-sys" ··· 308 765 ] 309 766 310 767 [[package]] 768 + name = "linux-raw-sys" 769 + version = "0.11.0" 770 + source = "registry+https://github.com/rust-lang/crates.io-index" 771 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 772 + 773 + [[package]] 774 + name = "litemap" 775 + version = "0.8.1" 776 + source = "registry+https://github.com/rust-lang/crates.io-index" 777 + checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" 778 + 779 + [[package]] 311 780 name = "log" 312 781 version = "0.4.28" 313 782 source = "registry+https://github.com/rust-lang/crates.io-index" 314 783 checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" 315 784 316 785 [[package]] 786 + name = "mediatype" 787 + version = "0.19.20" 788 + source = "registry+https://github.com/rust-lang/crates.io-index" 789 + checksum = "33746aadcb41349ec291e7f2f0a3aa6834d1d7c58066fb4b01f68efc4c4b7631" 790 + dependencies = [ 791 + "serde", 792 + ] 793 + 794 + [[package]] 795 + name = "memchr" 796 + version = "2.7.6" 797 + source = "registry+https://github.com/rust-lang/crates.io-index" 798 + checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 799 + 800 + [[package]] 801 + name = "mime" 802 + version = "0.3.17" 803 + source = "registry+https://github.com/rust-lang/crates.io-index" 804 + checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 805 + 806 + [[package]] 807 + name = "mio" 808 + version = "1.1.0" 809 + source = "registry+https://github.com/rust-lang/crates.io-index" 810 + checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" 811 + dependencies = [ 812 + "libc", 813 + "wasi", 814 + "windows-sys 0.61.2", 815 + ] 816 + 817 + [[package]] 818 + name = "native-tls" 819 + version = "0.2.14" 820 + source = "registry+https://github.com/rust-lang/crates.io-index" 821 + checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" 822 + dependencies = [ 823 + "libc", 824 + "log", 825 + "openssl", 826 + "openssl-probe", 827 + "openssl-sys", 828 + "schannel", 829 + "security-framework", 830 + "security-framework-sys", 831 + "tempfile", 832 + ] 833 + 834 + [[package]] 317 835 name = "num-traits" 318 836 version = "0.2.19" 319 837 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 335 853 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 336 854 337 855 [[package]] 856 + name = "openssl" 857 + version = "0.10.75" 858 + source = "registry+https://github.com/rust-lang/crates.io-index" 859 + checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" 860 + dependencies = [ 861 + "bitflags", 862 + "cfg-if", 863 + "foreign-types", 864 + "libc", 865 + "once_cell", 866 + "openssl-macros", 867 + "openssl-sys", 868 + ] 869 + 870 + [[package]] 871 + name = "openssl-macros" 872 + version = "0.1.1" 873 + source = "registry+https://github.com/rust-lang/crates.io-index" 874 + checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 875 + dependencies = [ 876 + "proc-macro2", 877 + "quote", 878 + "syn", 879 + ] 880 + 881 + [[package]] 882 + name = "openssl-probe" 883 + version = "0.1.6" 884 + source = "registry+https://github.com/rust-lang/crates.io-index" 885 + checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 886 + 887 + [[package]] 888 + name = "openssl-sys" 889 + version = "0.9.111" 890 + source = "registry+https://github.com/rust-lang/crates.io-index" 891 + checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" 892 + dependencies = [ 893 + "cc", 894 + "libc", 895 + "pkg-config", 896 + "vcpkg", 897 + ] 898 + 899 + [[package]] 338 900 name = "option-ext" 339 901 version = "0.2.0" 340 902 source = "registry+https://github.com/rust-lang/crates.io-index" 341 903 checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 342 904 343 905 [[package]] 906 + name = "owo-colors" 907 + version = "4.2.3" 908 + source = "registry+https://github.com/rust-lang/crates.io-index" 909 + checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" 910 + 911 + [[package]] 344 912 name = "pai" 345 913 version = "0.1.0" 346 914 dependencies = [ 347 915 "chrono", 348 916 "clap", 349 917 "dirs", 918 + "owo-colors", 350 919 "pai-core", 351 920 "rusqlite", 352 921 ] ··· 355 924 name = "pai-core" 356 925 version = "0.1.0" 357 926 dependencies = [ 927 + "chrono", 928 + "feed-rs", 929 + "reqwest", 930 + "serde", 931 + "serde_json", 358 932 "thiserror", 933 + "tokio", 934 + "toml", 359 935 ] 360 936 361 937 [[package]] ··· 363 939 version = "0.1.0" 364 940 365 941 [[package]] 942 + name = "percent-encoding" 943 + version = "2.3.2" 944 + source = "registry+https://github.com/rust-lang/crates.io-index" 945 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 946 + 947 + [[package]] 948 + name = "pin-project-lite" 949 + version = "0.2.16" 950 + source = "registry+https://github.com/rust-lang/crates.io-index" 951 + checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 952 + 953 + [[package]] 954 + name = "pin-utils" 955 + version = "0.1.0" 956 + source = "registry+https://github.com/rust-lang/crates.io-index" 957 + checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 958 + 959 + [[package]] 366 960 name = "pkg-config" 367 961 version = "0.3.32" 368 962 source = "registry+https://github.com/rust-lang/crates.io-index" 369 963 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 370 964 371 965 [[package]] 966 + name = "potential_utf" 967 + version = "0.1.4" 968 + source = "registry+https://github.com/rust-lang/crates.io-index" 969 + checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" 970 + dependencies = [ 971 + "zerovec", 972 + ] 973 + 974 + [[package]] 372 975 name = "proc-macro2" 373 976 version = "1.0.103" 374 977 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 378 981 ] 379 982 380 983 [[package]] 984 + name = "quick-xml" 985 + version = "0.37.5" 986 + source = "registry+https://github.com/rust-lang/crates.io-index" 987 + checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" 988 + dependencies = [ 989 + "encoding_rs", 990 + "memchr", 991 + ] 992 + 993 + [[package]] 381 994 name = "quote" 382 995 version = "1.0.42" 383 996 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 387 1000 ] 388 1001 389 1002 [[package]] 1003 + name = "r-efi" 1004 + version = "5.3.0" 1005 + source = "registry+https://github.com/rust-lang/crates.io-index" 1006 + checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" 1007 + 1008 + [[package]] 390 1009 name = "redox_users" 391 1010 version = "0.5.2" 392 1011 source = "registry+https://github.com/rust-lang/crates.io-index" 393 1012 checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" 394 1013 dependencies = [ 395 - "getrandom", 1014 + "getrandom 0.2.16", 396 1015 "libredox", 397 1016 "thiserror", 398 1017 ] 399 1018 400 1019 [[package]] 1020 + name = "regex" 1021 + version = "1.12.2" 1022 + source = "registry+https://github.com/rust-lang/crates.io-index" 1023 + checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" 1024 + dependencies = [ 1025 + "aho-corasick", 1026 + "memchr", 1027 + "regex-automata", 1028 + "regex-syntax", 1029 + ] 1030 + 1031 + [[package]] 1032 + name = "regex-automata" 1033 + version = "0.4.13" 1034 + source = "registry+https://github.com/rust-lang/crates.io-index" 1035 + checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 1036 + dependencies = [ 1037 + "aho-corasick", 1038 + "memchr", 1039 + "regex-syntax", 1040 + ] 1041 + 1042 + [[package]] 1043 + name = "regex-syntax" 1044 + version = "0.8.8" 1045 + source = "registry+https://github.com/rust-lang/crates.io-index" 1046 + checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" 1047 + 1048 + [[package]] 1049 + name = "reqwest" 1050 + version = "0.12.24" 1051 + source = "registry+https://github.com/rust-lang/crates.io-index" 1052 + checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" 1053 + dependencies = [ 1054 + "base64", 1055 + "bytes", 1056 + "encoding_rs", 1057 + "futures-core", 1058 + "h2", 1059 + "http", 1060 + "http-body", 1061 + "http-body-util", 1062 + "hyper", 1063 + "hyper-rustls", 1064 + "hyper-tls", 1065 + "hyper-util", 1066 + "js-sys", 1067 + "log", 1068 + "mime", 1069 + "native-tls", 1070 + "percent-encoding", 1071 + "pin-project-lite", 1072 + "rustls-pki-types", 1073 + "serde", 1074 + "serde_json", 1075 + "serde_urlencoded", 1076 + "sync_wrapper", 1077 + "tokio", 1078 + "tokio-native-tls", 1079 + "tower", 1080 + "tower-http", 1081 + "tower-service", 1082 + "url", 1083 + "wasm-bindgen", 1084 + "wasm-bindgen-futures", 1085 + "web-sys", 1086 + ] 1087 + 1088 + [[package]] 1089 + name = "ring" 1090 + version = "0.17.14" 1091 + source = "registry+https://github.com/rust-lang/crates.io-index" 1092 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 1093 + dependencies = [ 1094 + "cc", 1095 + "cfg-if", 1096 + "getrandom 0.2.16", 1097 + "libc", 1098 + "untrusted", 1099 + "windows-sys 0.52.0", 1100 + ] 1101 + 1102 + [[package]] 401 1103 name = "rusqlite" 402 1104 version = "0.37.0" 403 1105 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 412 1114 ] 413 1115 414 1116 [[package]] 1117 + name = "rustix" 1118 + version = "1.1.2" 1119 + source = "registry+https://github.com/rust-lang/crates.io-index" 1120 + checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 1121 + dependencies = [ 1122 + "bitflags", 1123 + "errno", 1124 + "libc", 1125 + "linux-raw-sys", 1126 + "windows-sys 0.61.2", 1127 + ] 1128 + 1129 + [[package]] 1130 + name = "rustls" 1131 + version = "0.23.35" 1132 + source = "registry+https://github.com/rust-lang/crates.io-index" 1133 + checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" 1134 + dependencies = [ 1135 + "once_cell", 1136 + "rustls-pki-types", 1137 + "rustls-webpki", 1138 + "subtle", 1139 + "zeroize", 1140 + ] 1141 + 1142 + [[package]] 1143 + name = "rustls-pki-types" 1144 + version = "1.13.0" 1145 + source = "registry+https://github.com/rust-lang/crates.io-index" 1146 + checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" 1147 + dependencies = [ 1148 + "zeroize", 1149 + ] 1150 + 1151 + [[package]] 1152 + name = "rustls-webpki" 1153 + version = "0.103.8" 1154 + source = "registry+https://github.com/rust-lang/crates.io-index" 1155 + checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" 1156 + dependencies = [ 1157 + "ring", 1158 + "rustls-pki-types", 1159 + "untrusted", 1160 + ] 1161 + 1162 + [[package]] 415 1163 name = "rustversion" 416 1164 version = "1.0.22" 417 1165 source = "registry+https://github.com/rust-lang/crates.io-index" 418 1166 checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" 419 1167 420 1168 [[package]] 1169 + name = "ryu" 1170 + version = "1.0.20" 1171 + source = "registry+https://github.com/rust-lang/crates.io-index" 1172 + checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1173 + 1174 + [[package]] 1175 + name = "schannel" 1176 + version = "0.1.28" 1177 + source = "registry+https://github.com/rust-lang/crates.io-index" 1178 + checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" 1179 + dependencies = [ 1180 + "windows-sys 0.61.2", 1181 + ] 1182 + 1183 + [[package]] 1184 + name = "security-framework" 1185 + version = "2.11.1" 1186 + source = "registry+https://github.com/rust-lang/crates.io-index" 1187 + checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1188 + dependencies = [ 1189 + "bitflags", 1190 + "core-foundation", 1191 + "core-foundation-sys", 1192 + "libc", 1193 + "security-framework-sys", 1194 + ] 1195 + 1196 + [[package]] 1197 + name = "security-framework-sys" 1198 + version = "2.15.0" 1199 + source = "registry+https://github.com/rust-lang/crates.io-index" 1200 + checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" 1201 + dependencies = [ 1202 + "core-foundation-sys", 1203 + "libc", 1204 + ] 1205 + 1206 + [[package]] 1207 + name = "serde" 1208 + version = "1.0.228" 1209 + source = "registry+https://github.com/rust-lang/crates.io-index" 1210 + checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 1211 + dependencies = [ 1212 + "serde_core", 1213 + "serde_derive", 1214 + ] 1215 + 1216 + [[package]] 1217 + name = "serde_core" 1218 + version = "1.0.228" 1219 + source = "registry+https://github.com/rust-lang/crates.io-index" 1220 + checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 1221 + dependencies = [ 1222 + "serde_derive", 1223 + ] 1224 + 1225 + [[package]] 1226 + name = "serde_derive" 1227 + version = "1.0.228" 1228 + source = "registry+https://github.com/rust-lang/crates.io-index" 1229 + checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 1230 + dependencies = [ 1231 + "proc-macro2", 1232 + "quote", 1233 + "syn", 1234 + ] 1235 + 1236 + [[package]] 1237 + name = "serde_json" 1238 + version = "1.0.145" 1239 + source = "registry+https://github.com/rust-lang/crates.io-index" 1240 + checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" 1241 + dependencies = [ 1242 + "itoa", 1243 + "memchr", 1244 + "ryu", 1245 + "serde", 1246 + "serde_core", 1247 + ] 1248 + 1249 + [[package]] 1250 + name = "serde_spanned" 1251 + version = "1.0.3" 1252 + source = "registry+https://github.com/rust-lang/crates.io-index" 1253 + checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" 1254 + dependencies = [ 1255 + "serde_core", 1256 + ] 1257 + 1258 + [[package]] 1259 + name = "serde_urlencoded" 1260 + version = "0.7.1" 1261 + source = "registry+https://github.com/rust-lang/crates.io-index" 1262 + checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1263 + dependencies = [ 1264 + "form_urlencoded", 1265 + "itoa", 1266 + "ryu", 1267 + "serde", 1268 + ] 1269 + 1270 + [[package]] 421 1271 name = "shlex" 422 1272 version = "1.3.0" 423 1273 source = "registry+https://github.com/rust-lang/crates.io-index" 424 1274 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 425 1275 426 1276 [[package]] 1277 + name = "siphasher" 1278 + version = "1.0.1" 1279 + source = "registry+https://github.com/rust-lang/crates.io-index" 1280 + checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 1281 + 1282 + [[package]] 1283 + name = "slab" 1284 + version = "0.4.11" 1285 + source = "registry+https://github.com/rust-lang/crates.io-index" 1286 + checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" 1287 + 1288 + [[package]] 427 1289 name = "smallvec" 428 1290 version = "1.15.1" 429 1291 source = "registry+https://github.com/rust-lang/crates.io-index" 430 1292 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 431 1293 432 1294 [[package]] 1295 + name = "socket2" 1296 + version = "0.6.1" 1297 + source = "registry+https://github.com/rust-lang/crates.io-index" 1298 + checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" 1299 + dependencies = [ 1300 + "libc", 1301 + "windows-sys 0.60.2", 1302 + ] 1303 + 1304 + [[package]] 1305 + name = "stable_deref_trait" 1306 + version = "1.2.1" 1307 + source = "registry+https://github.com/rust-lang/crates.io-index" 1308 + checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" 1309 + 1310 + [[package]] 433 1311 name = "strsim" 434 1312 version = "0.11.1" 435 1313 source = "registry+https://github.com/rust-lang/crates.io-index" 436 1314 checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 437 1315 438 1316 [[package]] 1317 + name = "subtle" 1318 + version = "2.6.1" 1319 + source = "registry+https://github.com/rust-lang/crates.io-index" 1320 + checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 1321 + 1322 + [[package]] 439 1323 name = "syn" 440 1324 version = "2.0.111" 441 1325 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 447 1331 ] 448 1332 449 1333 [[package]] 1334 + name = "sync_wrapper" 1335 + version = "1.0.2" 1336 + source = "registry+https://github.com/rust-lang/crates.io-index" 1337 + checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 1338 + dependencies = [ 1339 + "futures-core", 1340 + ] 1341 + 1342 + [[package]] 1343 + name = "synstructure" 1344 + version = "0.13.2" 1345 + source = "registry+https://github.com/rust-lang/crates.io-index" 1346 + checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 1347 + dependencies = [ 1348 + "proc-macro2", 1349 + "quote", 1350 + "syn", 1351 + ] 1352 + 1353 + [[package]] 1354 + name = "system-configuration" 1355 + version = "0.6.1" 1356 + source = "registry+https://github.com/rust-lang/crates.io-index" 1357 + checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 1358 + dependencies = [ 1359 + "bitflags", 1360 + "core-foundation", 1361 + "system-configuration-sys", 1362 + ] 1363 + 1364 + [[package]] 1365 + name = "system-configuration-sys" 1366 + version = "0.6.0" 1367 + source = "registry+https://github.com/rust-lang/crates.io-index" 1368 + checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1369 + dependencies = [ 1370 + "core-foundation-sys", 1371 + "libc", 1372 + ] 1373 + 1374 + [[package]] 1375 + name = "tempfile" 1376 + version = "3.23.0" 1377 + source = "registry+https://github.com/rust-lang/crates.io-index" 1378 + checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" 1379 + dependencies = [ 1380 + "fastrand", 1381 + "getrandom 0.3.4", 1382 + "once_cell", 1383 + "rustix", 1384 + "windows-sys 0.61.2", 1385 + ] 1386 + 1387 + [[package]] 450 1388 name = "thiserror" 451 1389 version = "2.0.17" 452 1390 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 467 1405 ] 468 1406 469 1407 [[package]] 1408 + name = "tinystr" 1409 + version = "0.8.2" 1410 + source = "registry+https://github.com/rust-lang/crates.io-index" 1411 + checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" 1412 + dependencies = [ 1413 + "displaydoc", 1414 + "zerovec", 1415 + ] 1416 + 1417 + [[package]] 1418 + name = "tokio" 1419 + version = "1.48.0" 1420 + source = "registry+https://github.com/rust-lang/crates.io-index" 1421 + checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 1422 + dependencies = [ 1423 + "bytes", 1424 + "libc", 1425 + "mio", 1426 + "pin-project-lite", 1427 + "socket2", 1428 + "tokio-macros", 1429 + "windows-sys 0.61.2", 1430 + ] 1431 + 1432 + [[package]] 1433 + name = "tokio-macros" 1434 + version = "2.6.0" 1435 + source = "registry+https://github.com/rust-lang/crates.io-index" 1436 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 1437 + dependencies = [ 1438 + "proc-macro2", 1439 + "quote", 1440 + "syn", 1441 + ] 1442 + 1443 + [[package]] 1444 + name = "tokio-native-tls" 1445 + version = "0.3.1" 1446 + source = "registry+https://github.com/rust-lang/crates.io-index" 1447 + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1448 + dependencies = [ 1449 + "native-tls", 1450 + "tokio", 1451 + ] 1452 + 1453 + [[package]] 1454 + name = "tokio-rustls" 1455 + version = "0.26.4" 1456 + source = "registry+https://github.com/rust-lang/crates.io-index" 1457 + checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" 1458 + dependencies = [ 1459 + "rustls", 1460 + "tokio", 1461 + ] 1462 + 1463 + [[package]] 1464 + name = "tokio-util" 1465 + version = "0.7.17" 1466 + source = "registry+https://github.com/rust-lang/crates.io-index" 1467 + checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" 1468 + dependencies = [ 1469 + "bytes", 1470 + "futures-core", 1471 + "futures-sink", 1472 + "pin-project-lite", 1473 + "tokio", 1474 + ] 1475 + 1476 + [[package]] 1477 + name = "toml" 1478 + version = "0.9.8" 1479 + source = "registry+https://github.com/rust-lang/crates.io-index" 1480 + checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" 1481 + dependencies = [ 1482 + "indexmap", 1483 + "serde_core", 1484 + "serde_spanned", 1485 + "toml_datetime", 1486 + "toml_parser", 1487 + "toml_writer", 1488 + "winnow", 1489 + ] 1490 + 1491 + [[package]] 1492 + name = "toml_datetime" 1493 + version = "0.7.3" 1494 + source = "registry+https://github.com/rust-lang/crates.io-index" 1495 + checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" 1496 + dependencies = [ 1497 + "serde_core", 1498 + ] 1499 + 1500 + [[package]] 1501 + name = "toml_parser" 1502 + version = "1.0.4" 1503 + source = "registry+https://github.com/rust-lang/crates.io-index" 1504 + checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" 1505 + dependencies = [ 1506 + "winnow", 1507 + ] 1508 + 1509 + [[package]] 1510 + name = "toml_writer" 1511 + version = "1.0.4" 1512 + source = "registry+https://github.com/rust-lang/crates.io-index" 1513 + checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" 1514 + 1515 + [[package]] 1516 + name = "tower" 1517 + version = "0.5.2" 1518 + source = "registry+https://github.com/rust-lang/crates.io-index" 1519 + checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1520 + dependencies = [ 1521 + "futures-core", 1522 + "futures-util", 1523 + "pin-project-lite", 1524 + "sync_wrapper", 1525 + "tokio", 1526 + "tower-layer", 1527 + "tower-service", 1528 + ] 1529 + 1530 + [[package]] 1531 + name = "tower-http" 1532 + version = "0.6.6" 1533 + source = "registry+https://github.com/rust-lang/crates.io-index" 1534 + checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 1535 + dependencies = [ 1536 + "bitflags", 1537 + "bytes", 1538 + "futures-util", 1539 + "http", 1540 + "http-body", 1541 + "iri-string", 1542 + "pin-project-lite", 1543 + "tower", 1544 + "tower-layer", 1545 + "tower-service", 1546 + ] 1547 + 1548 + [[package]] 1549 + name = "tower-layer" 1550 + version = "0.3.3" 1551 + source = "registry+https://github.com/rust-lang/crates.io-index" 1552 + checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1553 + 1554 + [[package]] 1555 + name = "tower-service" 1556 + version = "0.3.3" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1559 + 1560 + [[package]] 1561 + name = "tracing" 1562 + version = "0.1.41" 1563 + source = "registry+https://github.com/rust-lang/crates.io-index" 1564 + checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1565 + dependencies = [ 1566 + "pin-project-lite", 1567 + "tracing-core", 1568 + ] 1569 + 1570 + [[package]] 1571 + name = "tracing-core" 1572 + version = "0.1.34" 1573 + source = "registry+https://github.com/rust-lang/crates.io-index" 1574 + checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 1575 + dependencies = [ 1576 + "once_cell", 1577 + ] 1578 + 1579 + [[package]] 1580 + name = "try-lock" 1581 + version = "0.2.5" 1582 + source = "registry+https://github.com/rust-lang/crates.io-index" 1583 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1584 + 1585 + [[package]] 470 1586 name = "unicode-ident" 471 1587 version = "1.0.22" 472 1588 source = "registry+https://github.com/rust-lang/crates.io-index" 473 1589 checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 474 1590 475 1591 [[package]] 1592 + name = "untrusted" 1593 + version = "0.9.0" 1594 + source = "registry+https://github.com/rust-lang/crates.io-index" 1595 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1596 + 1597 + [[package]] 1598 + name = "url" 1599 + version = "2.5.7" 1600 + source = "registry+https://github.com/rust-lang/crates.io-index" 1601 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 1602 + dependencies = [ 1603 + "form_urlencoded", 1604 + "idna", 1605 + "percent-encoding", 1606 + "serde", 1607 + ] 1608 + 1609 + [[package]] 1610 + name = "utf8_iter" 1611 + version = "1.0.4" 1612 + source = "registry+https://github.com/rust-lang/crates.io-index" 1613 + checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1614 + 1615 + [[package]] 476 1616 name = "utf8parse" 477 1617 version = "0.2.2" 478 1618 source = "registry+https://github.com/rust-lang/crates.io-index" 479 1619 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 480 1620 481 1621 [[package]] 1622 + name = "uuid" 1623 + version = "1.18.1" 1624 + source = "registry+https://github.com/rust-lang/crates.io-index" 1625 + checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 1626 + dependencies = [ 1627 + "getrandom 0.3.4", 1628 + "js-sys", 1629 + "wasm-bindgen", 1630 + ] 1631 + 1632 + [[package]] 482 1633 name = "vcpkg" 483 1634 version = "0.2.15" 484 1635 source = "registry+https://github.com/rust-lang/crates.io-index" 485 1636 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 486 1637 487 1638 [[package]] 1639 + name = "want" 1640 + version = "0.3.1" 1641 + source = "registry+https://github.com/rust-lang/crates.io-index" 1642 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1643 + dependencies = [ 1644 + "try-lock", 1645 + ] 1646 + 1647 + [[package]] 488 1648 name = "wasi" 489 1649 version = "0.11.1+wasi-snapshot-preview1" 490 1650 source = "registry+https://github.com/rust-lang/crates.io-index" 491 1651 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 492 1652 493 1653 [[package]] 1654 + name = "wasip2" 1655 + version = "1.0.1+wasi-0.2.4" 1656 + source = "registry+https://github.com/rust-lang/crates.io-index" 1657 + checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" 1658 + dependencies = [ 1659 + "wit-bindgen", 1660 + ] 1661 + 1662 + [[package]] 494 1663 name = "wasm-bindgen" 495 1664 version = "0.2.105" 496 1665 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 504 1673 ] 505 1674 506 1675 [[package]] 1676 + name = "wasm-bindgen-futures" 1677 + version = "0.4.55" 1678 + source = "registry+https://github.com/rust-lang/crates.io-index" 1679 + checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" 1680 + dependencies = [ 1681 + "cfg-if", 1682 + "js-sys", 1683 + "once_cell", 1684 + "wasm-bindgen", 1685 + "web-sys", 1686 + ] 1687 + 1688 + [[package]] 507 1689 name = "wasm-bindgen-macro" 508 1690 version = "0.2.105" 509 1691 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 536 1718 ] 537 1719 538 1720 [[package]] 1721 + name = "web-sys" 1722 + version = "0.3.82" 1723 + source = "registry+https://github.com/rust-lang/crates.io-index" 1724 + checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" 1725 + dependencies = [ 1726 + "js-sys", 1727 + "wasm-bindgen", 1728 + ] 1729 + 1730 + [[package]] 539 1731 name = "windows-core" 540 1732 version = "0.62.2" 541 1733 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 577 1769 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 578 1770 579 1771 [[package]] 1772 + name = "windows-registry" 1773 + version = "0.6.1" 1774 + source = "registry+https://github.com/rust-lang/crates.io-index" 1775 + checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" 1776 + dependencies = [ 1777 + "windows-link", 1778 + "windows-result", 1779 + "windows-strings", 1780 + ] 1781 + 1782 + [[package]] 580 1783 name = "windows-result" 581 1784 version = "0.4.1" 582 1785 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 596 1799 597 1800 [[package]] 598 1801 name = "windows-sys" 1802 + version = "0.52.0" 1803 + source = "registry+https://github.com/rust-lang/crates.io-index" 1804 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1805 + dependencies = [ 1806 + "windows-targets 0.52.6", 1807 + ] 1808 + 1809 + [[package]] 1810 + name = "windows-sys" 1811 + version = "0.60.2" 1812 + source = "registry+https://github.com/rust-lang/crates.io-index" 1813 + checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 1814 + dependencies = [ 1815 + "windows-targets 0.53.5", 1816 + ] 1817 + 1818 + [[package]] 1819 + name = "windows-sys" 599 1820 version = "0.61.2" 600 1821 source = "registry+https://github.com/rust-lang/crates.io-index" 601 1822 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 602 1823 dependencies = [ 603 1824 "windows-link", 604 1825 ] 1826 + 1827 + [[package]] 1828 + name = "windows-targets" 1829 + version = "0.52.6" 1830 + source = "registry+https://github.com/rust-lang/crates.io-index" 1831 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1832 + dependencies = [ 1833 + "windows_aarch64_gnullvm 0.52.6", 1834 + "windows_aarch64_msvc 0.52.6", 1835 + "windows_i686_gnu 0.52.6", 1836 + "windows_i686_gnullvm 0.52.6", 1837 + "windows_i686_msvc 0.52.6", 1838 + "windows_x86_64_gnu 0.52.6", 1839 + "windows_x86_64_gnullvm 0.52.6", 1840 + "windows_x86_64_msvc 0.52.6", 1841 + ] 1842 + 1843 + [[package]] 1844 + name = "windows-targets" 1845 + version = "0.53.5" 1846 + source = "registry+https://github.com/rust-lang/crates.io-index" 1847 + checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 1848 + dependencies = [ 1849 + "windows-link", 1850 + "windows_aarch64_gnullvm 0.53.1", 1851 + "windows_aarch64_msvc 0.53.1", 1852 + "windows_i686_gnu 0.53.1", 1853 + "windows_i686_gnullvm 0.53.1", 1854 + "windows_i686_msvc 0.53.1", 1855 + "windows_x86_64_gnu 0.53.1", 1856 + "windows_x86_64_gnullvm 0.53.1", 1857 + "windows_x86_64_msvc 0.53.1", 1858 + ] 1859 + 1860 + [[package]] 1861 + name = "windows_aarch64_gnullvm" 1862 + version = "0.52.6" 1863 + source = "registry+https://github.com/rust-lang/crates.io-index" 1864 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1865 + 1866 + [[package]] 1867 + name = "windows_aarch64_gnullvm" 1868 + version = "0.53.1" 1869 + source = "registry+https://github.com/rust-lang/crates.io-index" 1870 + checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 1871 + 1872 + [[package]] 1873 + name = "windows_aarch64_msvc" 1874 + version = "0.52.6" 1875 + source = "registry+https://github.com/rust-lang/crates.io-index" 1876 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1877 + 1878 + [[package]] 1879 + name = "windows_aarch64_msvc" 1880 + version = "0.53.1" 1881 + source = "registry+https://github.com/rust-lang/crates.io-index" 1882 + checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 1883 + 1884 + [[package]] 1885 + name = "windows_i686_gnu" 1886 + version = "0.52.6" 1887 + source = "registry+https://github.com/rust-lang/crates.io-index" 1888 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1889 + 1890 + [[package]] 1891 + name = "windows_i686_gnu" 1892 + version = "0.53.1" 1893 + source = "registry+https://github.com/rust-lang/crates.io-index" 1894 + checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 1895 + 1896 + [[package]] 1897 + name = "windows_i686_gnullvm" 1898 + version = "0.52.6" 1899 + source = "registry+https://github.com/rust-lang/crates.io-index" 1900 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1901 + 1902 + [[package]] 1903 + name = "windows_i686_gnullvm" 1904 + version = "0.53.1" 1905 + source = "registry+https://github.com/rust-lang/crates.io-index" 1906 + checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 1907 + 1908 + [[package]] 1909 + name = "windows_i686_msvc" 1910 + version = "0.52.6" 1911 + source = "registry+https://github.com/rust-lang/crates.io-index" 1912 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1913 + 1914 + [[package]] 1915 + name = "windows_i686_msvc" 1916 + version = "0.53.1" 1917 + source = "registry+https://github.com/rust-lang/crates.io-index" 1918 + checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 1919 + 1920 + [[package]] 1921 + name = "windows_x86_64_gnu" 1922 + version = "0.52.6" 1923 + source = "registry+https://github.com/rust-lang/crates.io-index" 1924 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1925 + 1926 + [[package]] 1927 + name = "windows_x86_64_gnu" 1928 + version = "0.53.1" 1929 + source = "registry+https://github.com/rust-lang/crates.io-index" 1930 + checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 1931 + 1932 + [[package]] 1933 + name = "windows_x86_64_gnullvm" 1934 + version = "0.52.6" 1935 + source = "registry+https://github.com/rust-lang/crates.io-index" 1936 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1937 + 1938 + [[package]] 1939 + name = "windows_x86_64_gnullvm" 1940 + version = "0.53.1" 1941 + source = "registry+https://github.com/rust-lang/crates.io-index" 1942 + checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 1943 + 1944 + [[package]] 1945 + name = "windows_x86_64_msvc" 1946 + version = "0.52.6" 1947 + source = "registry+https://github.com/rust-lang/crates.io-index" 1948 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1949 + 1950 + [[package]] 1951 + name = "windows_x86_64_msvc" 1952 + version = "0.53.1" 1953 + source = "registry+https://github.com/rust-lang/crates.io-index" 1954 + checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 1955 + 1956 + [[package]] 1957 + name = "winnow" 1958 + version = "0.7.13" 1959 + source = "registry+https://github.com/rust-lang/crates.io-index" 1960 + checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" 1961 + 1962 + [[package]] 1963 + name = "wit-bindgen" 1964 + version = "0.46.0" 1965 + source = "registry+https://github.com/rust-lang/crates.io-index" 1966 + checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" 1967 + 1968 + [[package]] 1969 + name = "writeable" 1970 + version = "0.6.2" 1971 + source = "registry+https://github.com/rust-lang/crates.io-index" 1972 + checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" 1973 + 1974 + [[package]] 1975 + name = "yoke" 1976 + version = "0.8.1" 1977 + source = "registry+https://github.com/rust-lang/crates.io-index" 1978 + checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" 1979 + dependencies = [ 1980 + "stable_deref_trait", 1981 + "yoke-derive", 1982 + "zerofrom", 1983 + ] 1984 + 1985 + [[package]] 1986 + name = "yoke-derive" 1987 + version = "0.8.1" 1988 + source = "registry+https://github.com/rust-lang/crates.io-index" 1989 + checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" 1990 + dependencies = [ 1991 + "proc-macro2", 1992 + "quote", 1993 + "syn", 1994 + "synstructure", 1995 + ] 1996 + 1997 + [[package]] 1998 + name = "zerofrom" 1999 + version = "0.1.6" 2000 + source = "registry+https://github.com/rust-lang/crates.io-index" 2001 + checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 2002 + dependencies = [ 2003 + "zerofrom-derive", 2004 + ] 2005 + 2006 + [[package]] 2007 + name = "zerofrom-derive" 2008 + version = "0.1.6" 2009 + source = "registry+https://github.com/rust-lang/crates.io-index" 2010 + checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 2011 + dependencies = [ 2012 + "proc-macro2", 2013 + "quote", 2014 + "syn", 2015 + "synstructure", 2016 + ] 2017 + 2018 + [[package]] 2019 + name = "zeroize" 2020 + version = "1.8.2" 2021 + source = "registry+https://github.com/rust-lang/crates.io-index" 2022 + checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" 2023 + 2024 + [[package]] 2025 + name = "zerotrie" 2026 + version = "0.2.3" 2027 + source = "registry+https://github.com/rust-lang/crates.io-index" 2028 + checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" 2029 + dependencies = [ 2030 + "displaydoc", 2031 + "yoke", 2032 + "zerofrom", 2033 + ] 2034 + 2035 + [[package]] 2036 + name = "zerovec" 2037 + version = "0.11.5" 2038 + source = "registry+https://github.com/rust-lang/crates.io-index" 2039 + checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" 2040 + dependencies = [ 2041 + "yoke", 2042 + "zerofrom", 2043 + "zerovec-derive", 2044 + ] 2045 + 2046 + [[package]] 2047 + name = "zerovec-derive" 2048 + version = "0.11.2" 2049 + source = "registry+https://github.com/rust-lang/crates.io-index" 2050 + checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" 2051 + dependencies = [ 2052 + "proc-macro2", 2053 + "quote", 2054 + "syn", 2055 + ]
+661
LICENSE
··· 1 + GNU AFFERO GENERAL PUBLIC LICENSE 2 + Version 3, 19 November 2007 3 + 4 + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> 5 + Everyone is permitted to copy and distribute verbatim copies 6 + of this license document, but changing it is not allowed. 7 + 8 + Preamble 9 + 10 + The GNU Affero General Public License is a free, copyleft license for 11 + software and other kinds of works, specifically designed to ensure 12 + cooperation with the community in the case of network server software. 13 + 14 + The licenses for most software and other practical works are designed 15 + to take away your freedom to share and change the works. By contrast, 16 + our General Public Licenses are intended to guarantee your freedom to 17 + share and change all versions of a program--to make sure it remains free 18 + software for all its users. 19 + 20 + When we speak of free software, we are referring to freedom, not 21 + price. Our General Public Licenses are designed to make sure that you 22 + have the freedom to distribute copies of free software (and charge for 23 + them if you wish), that you receive source code or can get it if you 24 + want it, that you can change the software or use pieces of it in new 25 + free programs, and that you know you can do these things. 26 + 27 + Developers that use our General Public Licenses protect your rights 28 + with two steps: (1) assert copyright on the software, and (2) offer 29 + you this License which gives you legal permission to copy, distribute 30 + and/or modify the software. 31 + 32 + A secondary benefit of defending all users' freedom is that 33 + improvements made in alternate versions of the program, if they 34 + receive widespread use, become available for other developers to 35 + incorporate. Many developers of free software are heartened and 36 + encouraged by the resulting cooperation. However, in the case of 37 + software used on network servers, this result may fail to come about. 38 + The GNU General Public License permits making a modified version and 39 + letting the public access it on a server without ever releasing its 40 + source code to the public. 41 + 42 + The GNU Affero General Public License is designed specifically to 43 + ensure that, in such cases, the modified source code becomes available 44 + to the community. It requires the operator of a network server to 45 + provide the source code of the modified version running there to the 46 + users of that server. Therefore, public use of a modified version, on 47 + a publicly accessible server, gives the public access to the source 48 + code of the modified version. 49 + 50 + An older license, called the Affero General Public License and 51 + published by Affero, was designed to accomplish similar goals. This is 52 + a different license, not a version of the Affero GPL, but Affero has 53 + released a new version of the Affero GPL which permits relicensing under 54 + this license. 55 + 56 + The precise terms and conditions for copying, distribution and 57 + modification follow. 58 + 59 + TERMS AND CONDITIONS 60 + 61 + 0. Definitions. 62 + 63 + "This License" refers to version 3 of the GNU Affero General Public License. 64 + 65 + "Copyright" also means copyright-like laws that apply to other kinds of 66 + works, such as semiconductor masks. 67 + 68 + "The Program" refers to any copyrightable work licensed under this 69 + License. Each licensee is addressed as "you". "Licensees" and 70 + "recipients" may be individuals or organizations. 71 + 72 + To "modify" a work means to copy from or adapt all or part of the work 73 + in a fashion requiring copyright permission, other than the making of an 74 + exact copy. The resulting work is called a "modified version" of the 75 + earlier work or a work "based on" the earlier work. 76 + 77 + A "covered work" means either the unmodified Program or a work based 78 + on the Program. 79 + 80 + To "propagate" a work means to do anything with it that, without 81 + permission, would make you directly or secondarily liable for 82 + infringement under applicable copyright law, except executing it on a 83 + computer or modifying a private copy. Propagation includes copying, 84 + distribution (with or without modification), making available to the 85 + public, and in some countries other activities as well. 86 + 87 + To "convey" a work means any kind of propagation that enables other 88 + parties to make or receive copies. Mere interaction with a user through 89 + a computer network, with no transfer of a copy, is not conveying. 90 + 91 + An interactive user interface displays "Appropriate Legal Notices" 92 + to the extent that it includes a convenient and prominently visible 93 + feature that (1) displays an appropriate copyright notice, and (2) 94 + tells the user that there is no warranty for the work (except to the 95 + extent that warranties are provided), that licensees may convey the 96 + work under this License, and how to view a copy of this License. If 97 + the interface presents a list of user commands or options, such as a 98 + menu, a prominent item in the list meets this criterion. 99 + 100 + 1. Source Code. 101 + 102 + The "source code" for a work means the preferred form of the work 103 + for making modifications to it. "Object code" means any non-source 104 + form of a work. 105 + 106 + A "Standard Interface" means an interface that either is an official 107 + standard defined by a recognized standards body, or, in the case of 108 + interfaces specified for a particular programming language, one that 109 + is widely used among developers working in that language. 110 + 111 + The "System Libraries" of an executable work include anything, other 112 + than the work as a whole, that (a) is included in the normal form of 113 + packaging a Major Component, but which is not part of that Major 114 + Component, and (b) serves only to enable use of the work with that 115 + Major Component, or to implement a Standard Interface for which an 116 + implementation is available to the public in source code form. A 117 + "Major Component", in this context, means a major essential component 118 + (kernel, window system, and so on) of the specific operating system 119 + (if any) on which the executable work runs, or a compiler used to 120 + produce the work, or an object code interpreter used to run it. 121 + 122 + The "Corresponding Source" for a work in object code form means all 123 + the source code needed to generate, install, and (for an executable 124 + work) run the object code and to modify the work, including scripts to 125 + control those activities. However, it does not include the work's 126 + System Libraries, or general-purpose tools or generally available free 127 + programs which are used unmodified in performing those activities but 128 + which are not part of the work. For example, Corresponding Source 129 + includes interface definition files associated with source files for 130 + the work, and the source code for shared libraries and dynamically 131 + linked subprograms that the work is specifically designed to require, 132 + such as by intimate data communication or control flow between those 133 + subprograms and other parts of the work. 134 + 135 + The Corresponding Source need not include anything that users 136 + can regenerate automatically from other parts of the Corresponding 137 + Source. 138 + 139 + The Corresponding Source for a work in source code form is that 140 + same work. 141 + 142 + 2. Basic Permissions. 143 + 144 + All rights granted under this License are granted for the term of 145 + copyright on the Program, and are irrevocable provided the stated 146 + conditions are met. This License explicitly affirms your unlimited 147 + permission to run the unmodified Program. The output from running a 148 + covered work is covered by this License only if the output, given its 149 + content, constitutes a covered work. This License acknowledges your 150 + rights of fair use or other equivalent, as provided by copyright law. 151 + 152 + You may make, run and propagate covered works that you do not 153 + convey, without conditions so long as your license otherwise remains 154 + in force. You may convey covered works to others for the sole purpose 155 + of having them make modifications exclusively for you, or provide you 156 + with facilities for running those works, provided that you comply with 157 + the terms of this License in conveying all material for which you do 158 + not control copyright. Those thus making or running the covered works 159 + for you must do so exclusively on your behalf, under your direction 160 + and control, on terms that prohibit them from making any copies of 161 + your copyrighted material outside their relationship with you. 162 + 163 + Conveying under any other circumstances is permitted solely under 164 + the conditions stated below. Sublicensing is not allowed; section 10 165 + makes it unnecessary. 166 + 167 + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 + 169 + No covered work shall be deemed part of an effective technological 170 + measure under any applicable law fulfilling obligations under article 171 + 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 + similar laws prohibiting or restricting circumvention of such 173 + measures. 174 + 175 + When you convey a covered work, you waive any legal power to forbid 176 + circumvention of technological measures to the extent such circumvention 177 + is effected by exercising rights under this License with respect to 178 + the covered work, and you disclaim any intention to limit operation or 179 + modification of the work as a means of enforcing, against the work's 180 + users, your or third parties' legal rights to forbid circumvention of 181 + technological measures. 182 + 183 + 4. Conveying Verbatim Copies. 184 + 185 + You may convey verbatim copies of the Program's source code as you 186 + receive it, in any medium, provided that you conspicuously and 187 + appropriately publish on each copy an appropriate copyright notice; 188 + keep intact all notices stating that this License and any 189 + non-permissive terms added in accord with section 7 apply to the code; 190 + keep intact all notices of the absence of any warranty; and give all 191 + recipients a copy of this License along with the Program. 192 + 193 + You may charge any price or no price for each copy that you convey, 194 + and you may offer support or warranty protection for a fee. 195 + 196 + 5. Conveying Modified Source Versions. 197 + 198 + You may convey a work based on the Program, or the modifications to 199 + produce it from the Program, in the form of source code under the 200 + terms of section 4, provided that you also meet all of these conditions: 201 + 202 + a) The work must carry prominent notices stating that you modified 203 + it, and giving a relevant date. 204 + 205 + b) The work must carry prominent notices stating that it is 206 + released under this License and any conditions added under section 207 + 7. This requirement modifies the requirement in section 4 to 208 + "keep intact all notices". 209 + 210 + c) You must license the entire work, as a whole, under this 211 + License to anyone who comes into possession of a copy. This 212 + License will therefore apply, along with any applicable section 7 213 + additional terms, to the whole of the work, and all its parts, 214 + regardless of how they are packaged. This License gives no 215 + permission to license the work in any other way, but it does not 216 + invalidate such permission if you have separately received it. 217 + 218 + d) If the work has interactive user interfaces, each must display 219 + Appropriate Legal Notices; however, if the Program has interactive 220 + interfaces that do not display Appropriate Legal Notices, your 221 + work need not make them do so. 222 + 223 + A compilation of a covered work with other separate and independent 224 + works, which are not by their nature extensions of the covered work, 225 + and which are not combined with it such as to form a larger program, 226 + in or on a volume of a storage or distribution medium, is called an 227 + "aggregate" if the compilation and its resulting copyright are not 228 + used to limit the access or legal rights of the compilation's users 229 + beyond what the individual works permit. Inclusion of a covered work 230 + in an aggregate does not cause this License to apply to the other 231 + parts of the aggregate. 232 + 233 + 6. Conveying Non-Source Forms. 234 + 235 + You may convey a covered work in object code form under the terms 236 + of sections 4 and 5, provided that you also convey the 237 + machine-readable Corresponding Source under the terms of this License, 238 + in one of these ways: 239 + 240 + a) Convey the object code in, or embodied in, a physical product 241 + (including a physical distribution medium), accompanied by the 242 + Corresponding Source fixed on a durable physical medium 243 + customarily used for software interchange. 244 + 245 + b) Convey the object code in, or embodied in, a physical product 246 + (including a physical distribution medium), accompanied by a 247 + written offer, valid for at least three years and valid for as 248 + long as you offer spare parts or customer support for that product 249 + model, to give anyone who possesses the object code either (1) a 250 + copy of the Corresponding Source for all the software in the 251 + product that is covered by this License, on a durable physical 252 + medium customarily used for software interchange, for a price no 253 + more than your reasonable cost of physically performing this 254 + conveying of source, or (2) access to copy the 255 + Corresponding Source from a network server at no charge. 256 + 257 + c) Convey individual copies of the object code with a copy of the 258 + written offer to provide the Corresponding Source. This 259 + alternative is allowed only occasionally and noncommercially, and 260 + only if you received the object code with such an offer, in accord 261 + with subsection 6b. 262 + 263 + d) Convey the object code by offering access from a designated 264 + place (gratis or for a charge), and offer equivalent access to the 265 + Corresponding Source in the same way through the same place at no 266 + further charge. You need not require recipients to copy the 267 + Corresponding Source along with the object code. If the place to 268 + copy the object code is a network server, the Corresponding Source 269 + may be on a different server (operated by you or a third party) 270 + that supports equivalent copying facilities, provided you maintain 271 + clear directions next to the object code saying where to find the 272 + Corresponding Source. Regardless of what server hosts the 273 + Corresponding Source, you remain obligated to ensure that it is 274 + available for as long as needed to satisfy these requirements. 275 + 276 + e) Convey the object code using peer-to-peer transmission, provided 277 + you inform other peers where the object code and Corresponding 278 + Source of the work are being offered to the general public at no 279 + charge under subsection 6d. 280 + 281 + A separable portion of the object code, whose source code is excluded 282 + from the Corresponding Source as a System Library, need not be 283 + included in conveying the object code work. 284 + 285 + A "User Product" is either (1) a "consumer product", which means any 286 + tangible personal property which is normally used for personal, family, 287 + or household purposes, or (2) anything designed or sold for incorporation 288 + into a dwelling. In determining whether a product is a consumer product, 289 + doubtful cases shall be resolved in favor of coverage. For a particular 290 + product received by a particular user, "normally used" refers to a 291 + typical or common use of that class of product, regardless of the status 292 + of the particular user or of the way in which the particular user 293 + actually uses, or expects or is expected to use, the product. A product 294 + is a consumer product regardless of whether the product has substantial 295 + commercial, industrial or non-consumer uses, unless such uses represent 296 + the only significant mode of use of the product. 297 + 298 + "Installation Information" for a User Product means any methods, 299 + procedures, authorization keys, or other information required to install 300 + and execute modified versions of a covered work in that User Product from 301 + a modified version of its Corresponding Source. The information must 302 + suffice to ensure that the continued functioning of the modified object 303 + code is in no case prevented or interfered with solely because 304 + modification has been made. 305 + 306 + If you convey an object code work under this section in, or with, or 307 + specifically for use in, a User Product, and the conveying occurs as 308 + part of a transaction in which the right of possession and use of the 309 + User Product is transferred to the recipient in perpetuity or for a 310 + fixed term (regardless of how the transaction is characterized), the 311 + Corresponding Source conveyed under this section must be accompanied 312 + by the Installation Information. But this requirement does not apply 313 + if neither you nor any third party retains the ability to install 314 + modified object code on the User Product (for example, the work has 315 + been installed in ROM). 316 + 317 + The requirement to provide Installation Information does not include a 318 + requirement to continue to provide support service, warranty, or updates 319 + for a work that has been modified or installed by the recipient, or for 320 + the User Product in which it has been modified or installed. Access to a 321 + network may be denied when the modification itself materially and 322 + adversely affects the operation of the network or violates the rules and 323 + protocols for communication across the network. 324 + 325 + Corresponding Source conveyed, and Installation Information provided, 326 + in accord with this section must be in a format that is publicly 327 + documented (and with an implementation available to the public in 328 + source code form), and must require no special password or key for 329 + unpacking, reading or copying. 330 + 331 + 7. Additional Terms. 332 + 333 + "Additional permissions" are terms that supplement the terms of this 334 + License by making exceptions from one or more of its conditions. 335 + Additional permissions that are applicable to the entire Program shall 336 + be treated as though they were included in this License, to the extent 337 + that they are valid under applicable law. If additional permissions 338 + apply only to part of the Program, that part may be used separately 339 + under those permissions, but the entire Program remains governed by 340 + this License without regard to the additional permissions. 341 + 342 + When you convey a copy of a covered work, you may at your option 343 + remove any additional permissions from that copy, or from any part of 344 + it. (Additional permissions may be written to require their own 345 + removal in certain cases when you modify the work.) You may place 346 + additional permissions on material, added by you to a covered work, 347 + for which you have or can give appropriate copyright permission. 348 + 349 + Notwithstanding any other provision of this License, for material you 350 + add to a covered work, you may (if authorized by the copyright holders of 351 + that material) supplement the terms of this License with terms: 352 + 353 + a) Disclaiming warranty or limiting liability differently from the 354 + terms of sections 15 and 16 of this License; or 355 + 356 + b) Requiring preservation of specified reasonable legal notices or 357 + author attributions in that material or in the Appropriate Legal 358 + Notices displayed by works containing it; or 359 + 360 + c) Prohibiting misrepresentation of the origin of that material, or 361 + requiring that modified versions of such material be marked in 362 + reasonable ways as different from the original version; or 363 + 364 + d) Limiting the use for publicity purposes of names of licensors or 365 + authors of the material; or 366 + 367 + e) Declining to grant rights under trademark law for use of some 368 + trade names, trademarks, or service marks; or 369 + 370 + f) Requiring indemnification of licensors and authors of that 371 + material by anyone who conveys the material (or modified versions of 372 + it) with contractual assumptions of liability to the recipient, for 373 + any liability that these contractual assumptions directly impose on 374 + those licensors and authors. 375 + 376 + All other non-permissive additional terms are considered "further 377 + restrictions" within the meaning of section 10. If the Program as you 378 + received it, or any part of it, contains a notice stating that it is 379 + governed by this License along with a term that is a further 380 + restriction, you may remove that term. If a license document contains 381 + a further restriction but permits relicensing or conveying under this 382 + License, you may add to a covered work material governed by the terms 383 + of that license document, provided that the further restriction does 384 + not survive such relicensing or conveying. 385 + 386 + If you add terms to a covered work in accord with this section, you 387 + must place, in the relevant source files, a statement of the 388 + additional terms that apply to those files, or a notice indicating 389 + where to find the applicable terms. 390 + 391 + Additional terms, permissive or non-permissive, may be stated in the 392 + form of a separately written license, or stated as exceptions; 393 + the above requirements apply either way. 394 + 395 + 8. Termination. 396 + 397 + You may not propagate or modify a covered work except as expressly 398 + provided under this License. Any attempt otherwise to propagate or 399 + modify it is void, and will automatically terminate your rights under 400 + this License (including any patent licenses granted under the third 401 + paragraph of section 11). 402 + 403 + However, if you cease all violation of this License, then your 404 + license from a particular copyright holder is reinstated (a) 405 + provisionally, unless and until the copyright holder explicitly and 406 + finally terminates your license, and (b) permanently, if the copyright 407 + holder fails to notify you of the violation by some reasonable means 408 + prior to 60 days after the cessation. 409 + 410 + Moreover, your license from a particular copyright holder is 411 + reinstated permanently if the copyright holder notifies you of the 412 + violation by some reasonable means, this is the first time you have 413 + received notice of violation of this License (for any work) from that 414 + copyright holder, and you cure the violation prior to 30 days after 415 + your receipt of the notice. 416 + 417 + Termination of your rights under this section does not terminate the 418 + licenses of parties who have received copies or rights from you under 419 + this License. If your rights have been terminated and not permanently 420 + reinstated, you do not qualify to receive new licenses for the same 421 + material under section 10. 422 + 423 + 9. Acceptance Not Required for Having Copies. 424 + 425 + You are not required to accept this License in order to receive or 426 + run a copy of the Program. Ancillary propagation of a covered work 427 + occurring solely as a consequence of using peer-to-peer transmission 428 + to receive a copy likewise does not require acceptance. However, 429 + nothing other than this License grants you permission to propagate or 430 + modify any covered work. These actions infringe copyright if you do 431 + not accept this License. Therefore, by modifying or propagating a 432 + covered work, you indicate your acceptance of this License to do so. 433 + 434 + 10. Automatic Licensing of Downstream Recipients. 435 + 436 + Each time you convey a covered work, the recipient automatically 437 + receives a license from the original licensors, to run, modify and 438 + propagate that work, subject to this License. You are not responsible 439 + for enforcing compliance by third parties with this License. 440 + 441 + An "entity transaction" is a transaction transferring control of an 442 + organization, or substantially all assets of one, or subdividing an 443 + organization, or merging organizations. If propagation of a covered 444 + work results from an entity transaction, each party to that 445 + transaction who receives a copy of the work also receives whatever 446 + licenses to the work the party's predecessor in interest had or could 447 + give under the previous paragraph, plus a right to possession of the 448 + Corresponding Source of the work from the predecessor in interest, if 449 + the predecessor has it or can get it with reasonable efforts. 450 + 451 + You may not impose any further restrictions on the exercise of the 452 + rights granted or affirmed under this License. For example, you may 453 + not impose a license fee, royalty, or other charge for exercise of 454 + rights granted under this License, and you may not initiate litigation 455 + (including a cross-claim or counterclaim in a lawsuit) alleging that 456 + any patent claim is infringed by making, using, selling, offering for 457 + sale, or importing the Program or any portion of it. 458 + 459 + 11. Patents. 460 + 461 + A "contributor" is a copyright holder who authorizes use under this 462 + License of the Program or a work on which the Program is based. The 463 + work thus licensed is called the contributor's "contributor version". 464 + 465 + A contributor's "essential patent claims" are all patent claims 466 + owned or controlled by the contributor, whether already acquired or 467 + hereafter acquired, that would be infringed by some manner, permitted 468 + by this License, of making, using, or selling its contributor version, 469 + but do not include claims that would be infringed only as a 470 + consequence of further modification of the contributor version. For 471 + purposes of this definition, "control" includes the right to grant 472 + patent sublicenses in a manner consistent with the requirements of 473 + this License. 474 + 475 + Each contributor grants you a non-exclusive, worldwide, royalty-free 476 + patent license under the contributor's essential patent claims, to 477 + make, use, sell, offer for sale, import and otherwise run, modify and 478 + propagate the contents of its contributor version. 479 + 480 + In the following three paragraphs, a "patent license" is any express 481 + agreement or commitment, however denominated, not to enforce a patent 482 + (such as an express permission to practice a patent or covenant not to 483 + sue for patent infringement). To "grant" such a patent license to a 484 + party means to make such an agreement or commitment not to enforce a 485 + patent against the party. 486 + 487 + If you convey a covered work, knowingly relying on a patent license, 488 + and the Corresponding Source of the work is not available for anyone 489 + to copy, free of charge and under the terms of this License, through a 490 + publicly available network server or other readily accessible means, 491 + then you must either (1) cause the Corresponding Source to be so 492 + available, or (2) arrange to deprive yourself of the benefit of the 493 + patent license for this particular work, or (3) arrange, in a manner 494 + consistent with the requirements of this License, to extend the patent 495 + license to downstream recipients. "Knowingly relying" means you have 496 + actual knowledge that, but for the patent license, your conveying the 497 + covered work in a country, or your recipient's use of the covered work 498 + in a country, would infringe one or more identifiable patents in that 499 + country that you have reason to believe are valid. 500 + 501 + If, pursuant to or in connection with a single transaction or 502 + arrangement, you convey, or propagate by procuring conveyance of, a 503 + covered work, and grant a patent license to some of the parties 504 + receiving the covered work authorizing them to use, propagate, modify 505 + or convey a specific copy of the covered work, then the patent license 506 + you grant is automatically extended to all recipients of the covered 507 + work and works based on it. 508 + 509 + A patent license is "discriminatory" if it does not include within 510 + the scope of its coverage, prohibits the exercise of, or is 511 + conditioned on the non-exercise of one or more of the rights that are 512 + specifically granted under this License. You may not convey a covered 513 + work if you are a party to an arrangement with a third party that is 514 + in the business of distributing software, under which you make payment 515 + to the third party based on the extent of your activity of conveying 516 + the work, and under which the third party grants, to any of the 517 + parties who would receive the covered work from you, a discriminatory 518 + patent license (a) in connection with copies of the covered work 519 + conveyed by you (or copies made from those copies), or (b) primarily 520 + for and in connection with specific products or compilations that 521 + contain the covered work, unless you entered into that arrangement, 522 + or that patent license was granted, prior to 28 March 2007. 523 + 524 + Nothing in this License shall be construed as excluding or limiting 525 + any implied license or other defenses to infringement that may 526 + otherwise be available to you under applicable patent law. 527 + 528 + 12. No Surrender of Others' Freedom. 529 + 530 + If conditions are imposed on you (whether by court order, agreement or 531 + otherwise) that contradict the conditions of this License, they do not 532 + excuse you from the conditions of this License. If you cannot convey a 533 + covered work so as to satisfy simultaneously your obligations under this 534 + License and any other pertinent obligations, then as a consequence you may 535 + not convey it at all. For example, if you agree to terms that obligate you 536 + to collect a royalty for further conveying from those to whom you convey 537 + the Program, the only way you could satisfy both those terms and this 538 + License would be to refrain entirely from conveying the Program. 539 + 540 + 13. Remote Network Interaction; Use with the GNU General Public License. 541 + 542 + Notwithstanding any other provision of this License, if you modify the 543 + Program, your modified version must prominently offer all users 544 + interacting with it remotely through a computer network (if your version 545 + supports such interaction) an opportunity to receive the Corresponding 546 + Source of your version by providing access to the Corresponding Source 547 + from a network server at no charge, through some standard or customary 548 + means of facilitating copying of software. This Corresponding Source 549 + shall include the Corresponding Source for any work covered by version 3 550 + of the GNU General Public License that is incorporated pursuant to the 551 + following paragraph. 552 + 553 + Notwithstanding any other provision of this License, you have 554 + permission to link or combine any covered work with a work licensed 555 + under version 3 of the GNU General Public License into a single 556 + combined work, and to convey the resulting work. The terms of this 557 + License will continue to apply to the part which is the covered work, 558 + but the work with which it is combined will remain governed by version 559 + 3 of the GNU General Public License. 560 + 561 + 14. Revised Versions of this License. 562 + 563 + The Free Software Foundation may publish revised and/or new versions of 564 + the GNU Affero General Public License from time to time. Such new versions 565 + will be similar in spirit to the present version, but may differ in detail to 566 + address new problems or concerns. 567 + 568 + Each version is given a distinguishing version number. If the 569 + Program specifies that a certain numbered version of the GNU Affero General 570 + Public License "or any later version" applies to it, you have the 571 + option of following the terms and conditions either of that numbered 572 + version or of any later version published by the Free Software 573 + Foundation. If the Program does not specify a version number of the 574 + GNU Affero General Public License, you may choose any version ever published 575 + by the Free Software Foundation. 576 + 577 + If the Program specifies that a proxy can decide which future 578 + versions of the GNU Affero General Public License can be used, that proxy's 579 + public statement of acceptance of a version permanently authorizes you 580 + to choose that version for the Program. 581 + 582 + Later license versions may give you additional or different 583 + permissions. However, no additional obligations are imposed on any 584 + author or copyright holder as a result of your choosing to follow a 585 + later version. 586 + 587 + 15. Disclaimer of Warranty. 588 + 589 + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 + 598 + 16. Limitation of Liability. 599 + 600 + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 + GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 + USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 + DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 + PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 + EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 + SUCH DAMAGES. 609 + 610 + 17. Interpretation of Sections 15 and 16. 611 + 612 + If the disclaimer of warranty and limitation of liability provided 613 + above cannot be given local legal effect according to their terms, 614 + reviewing courts shall apply local law that most closely approximates 615 + an absolute waiver of all civil liability in connection with the 616 + Program, unless a warranty or assumption of liability accompanies a 617 + copy of the Program in return for a fee. 618 + 619 + END OF TERMS AND CONDITIONS 620 + 621 + How to Apply These Terms to Your New Programs 622 + 623 + If you develop a new program, and you want it to be of the greatest 624 + possible use to the public, the best way to achieve this is to make it 625 + free software which everyone can redistribute and change under these terms. 626 + 627 + To do so, attach the following notices to the program. It is safest 628 + to attach them to the start of each source file to most effectively 629 + state the exclusion of warranty; and each file should have at least 630 + the "copyright" line and a pointer to where the full notice is found. 631 + 632 + <one line to give the program's name and a brief idea of what it does.> 633 + Copyright (C) 2025 <name of author> 634 + 635 + This program is free software: you can redistribute it and/or modify 636 + it under the terms of the GNU Affero General Public License as published 637 + by the Free Software Foundation, either version 3 of the License, or 638 + (at your option) any later version. 639 + 640 + This program is distributed in the hope that it will be useful, 641 + but WITHOUT ANY WARRANTY; without even the implied warranty of 642 + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 + GNU Affero General Public License for more details. 644 + 645 + You should have received a copy of the GNU Affero General Public License 646 + along with this program. If not, see <https://www.gnu.org/licenses/>. 647 + 648 + Also add information on how to contact you by electronic and paper mail. 649 + 650 + If your software can interact with users remotely through a computer 651 + network, you should also make sure that it provides a way for users to 652 + get its source. For example, if your program is a web application, its 653 + interface could display a "Source" link that leads users to an archive 654 + of the code. There are many ways you could offer source, and different 655 + solutions will be better for different programs; see section 13 for the 656 + specific requirements. 657 + 658 + You should also get your employer (if you work as a programmer) or school, 659 + if any, to sign a "copyright disclaimer" for the program, if necessary. 660 + For more information on this, and how to apply and follow the GNU AGPL, see 661 + <https://www.gnu.org/licenses/>.
+207
README.md
··· 1 + <!-- markdownlint-disable MD033 --> 2 + 1 3 # Personal Activity Index 2 4 3 5 A CLI that ingests content from Substack, Bluesky, and Leaflet into SQLite, with an optional Cloudflare Worker + D1 deployment path. 6 + 7 + ## Features 8 + 9 + - Fetch posts from multiple sources: 10 + - **Substack** via RSS feeds 11 + - **Bluesky** via AT Protocol 12 + - **Leaflet** publications via RSS feeds 13 + - Local SQLite storage with full-text search 14 + - Flexible filtering and querying 15 + - Self-hostable or serverless (Cloudflare Workers) 16 + 17 + ## Quick Start 18 + 19 + ```bash 20 + # Install 21 + cargo install --path cli 22 + 23 + # Initialize config (creates ~/.config/pai/config.toml) 24 + pai init 25 + 26 + # Edit config with your sources 27 + $EDITOR ~/.config/pai/config.toml 28 + 29 + # Sync content 30 + pai sync 31 + 32 + # List items 33 + pai list -n 10 34 + 35 + # Check database 36 + pai db-check 37 + ``` 38 + 39 + ## Configuration 40 + 41 + Configuration is loaded from `$XDG_CONFIG_HOME/pai/config.toml` or `$HOME/.config/pai/config.toml`. 42 + 43 + See [config.example.toml](./config.example.toml) for a complete example with all available options. 44 + 45 + ## Architecture 46 + 47 + The project is organized as a Cargo workspace 48 + 49 + ```sh 50 + . 51 + ├── core # Shared types, fetchers, and the storage trait 52 + ├── cli # CLI binary (POSIX-compliant) 53 + └── worker # Cloudflare Worker deployment using workers-rs 54 + ``` 55 + 56 + <details> 57 + <summary><strong>Source Implementations</strong></summary> 58 + 59 + ### Substack (RSS) 60 + 61 + Substack fetcher uses standard RSS 2.0 feeds available at `{base_url}/feed`. 62 + 63 + **Implementation:** 64 + 65 + - Fetches RSS feed using `feed-rs` parser 66 + - Maps RSS `<item>` elements to standardized `Item` struct 67 + - Uses GUID as item ID, falls back to link if GUID is missing 68 + - Normalizes `pubDate` to ISO 8601 format 69 + 70 + **Key mappings:** 71 + 72 + - `id` = RSS GUID or link 73 + - `source_kind` = `substack` 74 + - `source_id` = Domain extracted from base_url 75 + - `title` = RSS title 76 + - `summary` = RSS description 77 + - `url` = RSS link 78 + - `content_html` = RSS content (if available) 79 + - `published_at` = RSS pubDate (normalized to ISO 8601) 80 + 81 + **Example RSS structure:** 82 + 83 + ```xml 84 + <item> 85 + <title>Post Title</title> 86 + <link>https://example.substack.com/p/post-slug</link> 87 + <guid>https://example.substack.com/p/post-slug</guid> 88 + <pubDate>Mon, 01 Jan 2024 12:00:00 +0000</pubDate> 89 + <description>Post summary or excerpt</description> 90 + </item> 91 + ``` 92 + 93 + ### AT Protocol Integration (Bluesky) 94 + 95 + #### Overview 96 + 97 + Bluesky is built on the AT Protocol (Authenticated Transfer Protocol), a decentralized social networking protocol. 98 + 99 + **Key Concepts:** 100 + 101 + - **DID (Decentralized Identifier)**: Unique identifier for users (e.g., `did:plc:xyz123`) 102 + - **Handle**: Human-readable identifier (e.g., `user.bsky.social`) 103 + - **AT URI**: Resource identifier (e.g., `at://did:plc:xyz/app.bsky.feed.post/abc123`) 104 + - **Lexicon**: Schema definition language for records and API methods 105 + - **XRPC**: HTTP API wrapper for AT Protocol methods 106 + - **PDS (Personal Data Server)**: Server that stores user data 107 + 108 + #### Implementation 109 + 110 + Bluesky uses standard `app.bsky.feed.post` records and provides a public API for fetching posts. 111 + 112 + **Endpoint:** `GET https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed` 113 + 114 + **Parameters:** 115 + 116 + - `actor` - User handle or DID 117 + - `limit` - Number of posts to fetch (default: 50) 118 + - `cursor` - Pagination cursor (optional) 119 + 120 + **Implementation:** 121 + 122 + - Fetches author feed using `app.bsky.feed.getAuthorFeed` 123 + - Filters out reposts and quotes (only includes original posts) 124 + - Converts AT URIs to canonical Bluesky URLs 125 + - Truncates long post text to create titles 126 + 127 + **Key mappings:** 128 + 129 + - `id` = AT URI (e.g., `at://did:plc:xyz/app.bsky.feed.post/abc123`) 130 + - `source_kind` = `bluesky` 131 + - `source_id` = User handle 132 + - `title` = Truncated post text (first 100 chars) 133 + - `summary` = Full post text 134 + - `url` = Canonical URL (`https://bsky.app/profile/{handle}/post/{post_id}`) 135 + - `author` = Post author handle 136 + - `published_at` = Post `createdAt` timestamp 137 + 138 + **Filtering reposts:** 139 + Posts with a `reason` field (indicating repost or quote) are excluded to fetch only original content. 140 + 141 + ### Leaflet (RSS) 142 + 143 + #### Overview 144 + 145 + Leaflet publications provide RSS feeds at `{base_url}/rss`, making them straightforward to fetch using standard RSS parsing. 146 + 147 + **Note:** While Leaflet is built on AT Protocol and uses custom `pub.leaflet.post` records, we use RSS feeds for simplicity and reliability. Leaflet's RSS implementation provides all necessary metadata without requiring AT Protocol PDS queries. 148 + 149 + **Implementation:** 150 + 151 + - Fetches RSS feed using `feed-rs` parser 152 + - Maps RSS `<item>` elements to standardized `Item` struct 153 + - Supports multiple publications via config array 154 + - Uses entry ID from feed, falls back to link if missing 155 + - Normalizes publication dates to ISO 8601 format 156 + 157 + **Key mappings:** 158 + 159 + - `id` = RSS entry ID or link 160 + - `source_kind` = `leaflet` 161 + - `source_id` = Publication ID from config (e.g., `desertthunder`, `stormlightlabs`) 162 + - `title` = RSS entry title 163 + - `summary` = RSS entry summary/description 164 + - `url` = RSS entry link 165 + - `content_html` = RSS content body (if available) 166 + - `author` = RSS entry author 167 + - `published_at` = RSS published date or updated date (normalized to ISO 8601) 168 + 169 + **Configuration:** 170 + 171 + Leaflet supports multiple publications through array configuration: 172 + 173 + ```toml 174 + [[sources.leaflet]] 175 + enabled = true 176 + id = "desertthunder" 177 + base_url = "https://desertthunder.leaflet.pub" 178 + 179 + [[sources.leaflet]] 180 + enabled = true 181 + id = "stormlightlabs" 182 + base_url = "https://stormlightlabs.leaflet.pub" 183 + ``` 184 + 185 + **Example RSS structure:** 186 + 187 + ```xml 188 + <item> 189 + <title>Dev Log: 2025-11-22</title> 190 + <link>https://desertthunder.leaflet.pub/3m6a7fuk7u22p</link> 191 + <guid>https://desertthunder.leaflet.pub/3m6a7fuk7u22p</guid> 192 + <pubDate>Fri, 22 Nov 2025 16:22:54 +0000</pubDate> 193 + <description>Post summary or excerpt</description> 194 + </item> 195 + ``` 196 + 197 + </details> 198 + 199 + ## References 200 + 201 + - [AT Protocol Documentation](https://atproto.com) 202 + - [Lexicon Guide](https://atproto.com/guides/lexicon) - Schema definition language 203 + - [XRPC Specification](https://atproto.com/specs/xrpc) - HTTP API wrapper 204 + - [Bluesky API Documentation](https://docs.bsky.app/) 205 + - [Leaflet](https://tangled.org/leaflet.pub/leaflet) - Leaflet source code 206 + - [Leaflet Manual](https://about.leaflet.pub/) - User-facing documentation 207 + 208 + ## License 209 + 210 + See [LICENSE file](./LICENSE) for details.
+1
cli/Cargo.toml
··· 13 13 rusqlite = { version = "0.37", features = ["bundled"] } 14 14 chrono = "0.4" 15 15 dirs = "6.0" 16 + owo-colors = "4.1"
+138 -58
cli/src/main.rs
··· 2 2 mod storage; 3 3 4 4 use clap::{Parser, Subcommand}; 5 + use owo_colors::OwoColorize; 5 6 use pai_core::{Config, ListFilter, PaiError, SourceKind}; 6 7 use std::path::PathBuf; 7 8 use storage::SqliteStorage; ··· 23 24 command: Commands, 24 25 } 25 26 27 + #[derive(Parser, Debug)] 28 + struct ExportOpts { 29 + /// Filter by source kind 30 + #[arg(short = 'k', value_name = "KIND")] 31 + kind: Option<SourceKind>, 32 + 33 + /// Filter by specific source ID 34 + #[arg(short = 'S', value_name = "ID")] 35 + source_id: Option<String>, 36 + 37 + /// Maximum number of items 38 + #[arg(short = 'n', value_name = "NUMBER")] 39 + limit: Option<usize>, 40 + 41 + /// Only items published at or after this time 42 + #[arg(short = 's', value_name = "TIME")] 43 + since: Option<String>, 44 + 45 + /// Filter items by substring 46 + #[arg(short = 'q', value_name = "PATTERN")] 47 + query: Option<String>, 48 + 49 + /// Output format 50 + #[arg(short = 'f', value_name = "FORMAT", default_value = "json")] 51 + format: String, 52 + 53 + /// Output file (default: stdout) 54 + #[arg(short = 'o', value_name = "FILE")] 55 + output: Option<PathBuf>, 56 + } 57 + 58 + impl From<ExportOpts> for ListFilter { 59 + fn from(opts: ExportOpts) -> Self { 60 + ListFilter { 61 + source_kind: opts.kind, 62 + source_id: opts.source_id, 63 + limit: opts.limit, 64 + since: opts.since, 65 + query: opts.query, 66 + } 67 + } 68 + } 69 + 26 70 #[derive(Subcommand, Debug)] 27 71 enum Commands { 28 72 /// Fetch and store content from configured sources ··· 64 108 }, 65 109 66 110 /// Produce feeds or export files 67 - Export { 68 - /// Filter by source kind 69 - #[arg(short = 'k', value_name = "KIND")] 70 - kind: Option<SourceKind>, 71 - 72 - /// Filter by specific source ID 73 - #[arg(short = 'S', value_name = "ID")] 74 - source_id: Option<String>, 75 - 76 - /// Maximum number of items 77 - #[arg(short = 'n', value_name = "NUMBER")] 78 - limit: Option<usize>, 79 - 80 - /// Only items published at or after this time 81 - #[arg(short = 's', value_name = "TIME")] 82 - since: Option<String>, 83 - 84 - /// Filter items by substring 85 - #[arg(short = 'q', value_name = "PATTERN")] 86 - query: Option<String>, 87 - 88 - /// Output format 89 - #[arg(short = 'f', value_name = "FORMAT", default_value = "json")] 90 - format: String, 91 - 92 - /// Output file (default: stdout) 93 - #[arg(short = 'o', value_name = "FILE")] 94 - output: Option<PathBuf>, 95 - }, 111 + Export(ExportOpts), 96 112 97 113 /// Self-host HTTP API 98 114 Serve { ··· 103 119 104 120 /// Verify database schema and print statistics 105 121 DbCheck, 122 + 123 + /// Initialize configuration file 124 + Init { 125 + /// Force overwrite existing config 126 + #[arg(short = 'f')] 127 + force: bool, 128 + }, 106 129 } 107 130 108 131 fn main() { ··· 113 136 Commands::List { kind, source_id, limit, since, query } => { 114 137 handle_list(cli.db_path, kind, source_id, limit, since, query) 115 138 } 116 - Commands::Export { kind, source_id, limit, since, query, format, output } => { 117 - handle_export(cli.db_path, kind, source_id, limit, since, query, format, output) 118 - } 139 + Commands::Export(opts) => handle_export(cli.db_path, opts), 119 140 Commands::Serve { address } => handle_serve(cli.db_path, address), 120 141 Commands::DbCheck => handle_db_check(cli.db_path), 142 + Commands::Init { force } => handle_init(cli.config_dir, force), 121 143 }; 122 144 123 145 if let Err(e) = result { 124 - eprintln!("Error: {e}"); 146 + eprintln!("{} {}", "Error:".red().bold(), e); 125 147 std::process::exit(1); 126 148 } 127 149 } 128 150 129 151 fn handle_sync( 130 - config_dir: Option<PathBuf>, db_path: Option<PathBuf>, _all: bool, _kind: Option<SourceKind>, 131 - _source_id: Option<String>, 152 + config_dir: Option<PathBuf>, db_path: Option<PathBuf>, _all: bool, kind: Option<SourceKind>, 153 + source_id: Option<String>, 132 154 ) -> Result<(), PaiError> { 133 155 let db_path = paths::resolve_db_path(db_path)?; 134 - let _config_dir = paths::resolve_config_dir(config_dir)?; 156 + let config_dir = paths::resolve_config_dir(config_dir)?; 135 157 136 158 let storage = SqliteStorage::new(db_path)?; 137 - let config = Config::default(); 138 159 139 - let count = pai_core::sync_all_sources(&config, &storage)?; 160 + let config_path = config_dir.join("config.toml"); 161 + let config = if config_path.exists() { 162 + Config::from_file(&config_path)? 163 + } else { 164 + println!( 165 + "{} No config file found, using default configuration", 166 + "Warning:".yellow() 167 + ); 168 + Config::default() 169 + }; 140 170 141 - println!("Synced {count} items"); 171 + let count = pai_core::sync_all_sources(&config, &storage, kind, source_id.as_deref())?; 172 + 173 + if count == 0 { 174 + println!("{} No sources synced (check your config or filters)", "Info:".cyan()); 175 + } else { 176 + println!("{} Synced {}", "Success:".green(), format!("{count} source(s)").bold()); 177 + } 178 + 142 179 Ok(()) 143 180 } 144 181 ··· 154 191 let items = pai_core::Storage::list_items(&storage, &filter)?; 155 192 156 193 if items.is_empty() { 157 - println!("No items found"); 194 + println!("{}", "No items found".yellow()); 158 195 return Ok(()); 159 196 } 160 197 161 - println!("Found {} items:\n", items.len()); 198 + println!("{} {}\n", "Found".cyan(), format!("{} items:", items.len()).bold()); 162 199 for item in items { 163 - println!("ID: {}", item.id); 164 - println!("Source: {} ({})", item.source_kind, item.source_id); 200 + println!("{} {}", "ID:".bright_black(), item.id); 201 + println!( 202 + "{} {} {}", 203 + "Source:".bright_black(), 204 + item.source_kind.to_string().cyan(), 205 + format!("({})", item.source_id).bright_black() 206 + ); 165 207 if let Some(title) = &item.title { 166 - println!("Title: {title}"); 208 + println!("{} {}", "Title:".bright_black(), title.bold()); 167 209 } 168 210 if let Some(author) = &item.author { 169 - println!("Author: {author}"); 211 + println!("{} {}", "Author:".bright_black(), author); 170 212 } 171 - println!("URL: {}", item.url); 172 - println!("Published: {}", item.published_at); 213 + println!("{} {}", "URL:".bright_black(), item.url.blue().underline()); 214 + println!("{} {}", "Published:".bright_black(), item.published_at); 173 215 println!(); 174 216 } 175 217 176 218 Ok(()) 177 219 } 178 220 179 - fn handle_export( 180 - db_path: Option<PathBuf>, kind: Option<SourceKind>, source_id: Option<String>, limit: Option<usize>, 181 - since: Option<String>, query: Option<String>, format: String, output: Option<PathBuf>, 182 - ) -> Result<(), PaiError> { 221 + fn handle_export(db_path: Option<PathBuf>, opts: ExportOpts) -> Result<(), PaiError> { 183 222 let db_path = paths::resolve_db_path(db_path)?; 184 223 let _storage = SqliteStorage::new(db_path)?; 185 224 186 - let filter = ListFilter { source_kind: kind, source_id, limit, since, query }; 225 + let format = opts.format.clone(); 226 + let output = opts.output.clone(); 227 + let filter: ListFilter = opts.into(); 187 228 188 229 println!("export command - format: {format}, output: {output:?}, filter: {filter:?}"); 189 230 Ok(()) ··· 201 242 let db_path = paths::resolve_db_path(db_path)?; 202 243 let storage = SqliteStorage::new(db_path)?; 203 244 204 - println!("Verifying database schema..."); 245 + println!("{}", "Verifying database schema...".cyan()); 205 246 storage.verify_schema()?; 206 - println!("Schema verification: OK\n"); 247 + println!("{} {}\n", "Schema verification:".green(), "OK".bold()); 207 248 208 - println!("Database statistics:"); 249 + println!("{}", "Database statistics:".cyan().bold()); 209 250 let total = storage.count_items()?; 210 - println!(" Total items: {total}"); 251 + println!(" {}: {}", "Total items".bright_black(), total.to_string().bold()); 211 252 212 253 let stats = storage.get_stats()?; 213 254 if !stats.is_empty() { 214 - println!("\nItems by source:"); 255 + println!("\n{}", "Items by source:".cyan().bold()); 215 256 for (source_kind, count) in stats { 216 - println!(" {source_kind}: {count}"); 257 + println!(" {}: {}", source_kind.bright_black(), count.to_string().bold()); 217 258 } 218 259 } 219 260 220 261 Ok(()) 221 262 } 263 + 264 + fn handle_init(config_dir: Option<PathBuf>, force: bool) -> Result<(), PaiError> { 265 + let config_dir = paths::resolve_config_dir(config_dir)?; 266 + let config_path = config_dir.join("config.toml"); 267 + 268 + if config_path.exists() && !force { 269 + println!( 270 + "{} Config file already exists at {}", 271 + "Error:".red().bold(), 272 + config_path.display() 273 + ); 274 + println!("{} Use {} to overwrite", "Hint:".yellow(), "pai init -f".bold()); 275 + return Err(PaiError::Config("Config file already exists".to_string())); 276 + } 277 + 278 + std::fs::create_dir_all(&config_dir) 279 + .map_err(|e| PaiError::Config(format!("Failed to create config directory: {e}")))?; 280 + 281 + let default_config = include_str!("../../config.example.toml"); 282 + std::fs::write(&config_path, default_config) 283 + .map_err(|e| PaiError::Config(format!("Failed to write config file: {e}")))?; 284 + 285 + println!("{} Created configuration file", "Success:".green().bold()); 286 + println!( 287 + " {}: {}", 288 + "Location".bright_black(), 289 + config_path.display().to_string().bold() 290 + ); 291 + println!(); 292 + println!("{}", "Next steps:".cyan().bold()); 293 + println!(" 1. Edit the config file to add your sources:"); 294 + println!(" {}", format!("$EDITOR {}", config_path.display()).bright_black()); 295 + println!(" 2. Run sync to fetch content:"); 296 + println!(" {}", "pai sync".bright_black()); 297 + println!(" 3. List your items:"); 298 + println!(" {}", "pai list -n 10".bright_black()); 299 + 300 + Ok(()) 301 + }
+41
config.example.toml
··· 1 + # Personal Activity Index Configuration Example 2 + # Copy this file to your config directory and customize as needed 3 + # 4 + # Default config location: 5 + # - $XDG_CONFIG_HOME/pai/config.toml 6 + # - $HOME/.config/pai/config.toml 7 + 8 + [database] 9 + # Path to SQLite database file (optional, defaults to $XDG_DATA_HOME/pai/pai.db) 10 + path = "/home/owais/.local/share/pai/pai.db" 11 + 12 + [deployment] 13 + # Deployment mode: "sqlite" for local, "cloudflare" for Workers 14 + mode = "sqlite" 15 + 16 + # Cloudflare deployment configuration (optional, only needed for Workers) 17 + [deployment.cloudflare] 18 + worker_name = "personal-activity-index" 19 + d1_binding = "DB" 20 + database_name = "personal_activity_db" 21 + 22 + # Substack RSS feed source 23 + [sources.substack] 24 + enabled = true 25 + base_url = "https://patternmatched.substack.com" 26 + 27 + # Bluesky AT Protocol source 28 + [sources.bluesky] 29 + enabled = true 30 + handle = "desertthunder.dev" 31 + 32 + # Leaflet publications (can have multiple) 33 + [[sources.leaflet]] 34 + enabled = true 35 + id = "desertthunder" 36 + base_url = "https://desertthunder.leaflet.pub" 37 + 38 + [[sources.leaflet]] 39 + enabled = true 40 + id = "stormlightlabs" 41 + base_url = "https://stormlightlabs.leaflet.pub"
+7
core/Cargo.toml
··· 5 5 6 6 [dependencies] 7 7 thiserror = "2.0.17" 8 + serde = { version = "1.0", features = ["derive"] } 9 + serde_json = "1.0" 10 + toml = "0.9" 11 + reqwest = { version = "0.12", features = ["json"] } 12 + tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } 13 + feed-rs = "2.2" 14 + chrono = "0.4"
+251
core/src/fetchers/bluesky.rs
··· 1 + use crate::{BlueskyConfig, Item, PaiError, Result, SourceFetcher, SourceKind, Storage}; 2 + use chrono::Utc; 3 + use serde::Deserialize; 4 + 5 + const BLUESKY_API_BASE: &str = "https://public.api.bsky.app"; 6 + 7 + /// Response from app.bsky.feed.getAuthorFeed 8 + #[derive(Debug, Deserialize)] 9 + struct AuthorFeedResponse { 10 + feed: Vec<FeedViewPost>, 11 + #[allow(dead_code)] 12 + cursor: Option<String>, 13 + } 14 + 15 + /// A post in the author feed 16 + #[derive(Debug, Deserialize)] 17 + struct FeedViewPost { 18 + post: PostView, 19 + #[allow(dead_code)] 20 + reason: Option<serde_json::Value>, 21 + } 22 + 23 + /// Post view with metadata 24 + #[derive(Debug, Deserialize)] 25 + struct PostView { 26 + uri: String, 27 + #[allow(dead_code)] 28 + cid: String, 29 + author: Author, 30 + record: serde_json::Value, 31 + #[allow(dead_code)] 32 + #[serde(rename = "indexedAt")] 33 + indexed_at: String, 34 + } 35 + 36 + /// Author information 37 + #[derive(Debug, Deserialize)] 38 + struct Author { 39 + #[allow(dead_code)] 40 + did: String, 41 + handle: String, 42 + #[allow(dead_code)] 43 + #[serde(rename = "displayName")] 44 + display_name: Option<String>, 45 + } 46 + 47 + /// Fetcher for Bluesky posts via AT Protocol 48 + /// 49 + /// Retrieves posts from a Bluesky user by querying the public API. 50 + /// Filters out reposts and quotes to only include original posts. 51 + pub struct BlueskyFetcher { 52 + config: BlueskyConfig, 53 + client: reqwest::Client, 54 + } 55 + 56 + impl BlueskyFetcher { 57 + /// Creates a new Bluesky fetcher with the given configuration 58 + pub fn new(config: BlueskyConfig) -> Self { 59 + Self { config, client: reqwest::Client::new() } 60 + } 61 + 62 + /// Fetches the author feed from the Bluesky public API 63 + async fn fetch_author_feed(&self) -> Result<AuthorFeedResponse> { 64 + let url = format!("{BLUESKY_API_BASE}/xrpc/app.bsky.feed.getAuthorFeed"); 65 + 66 + let response = self 67 + .client 68 + .get(&url) 69 + .query(&[("actor", &self.config.handle), ("limit", &"50".to_string())]) 70 + .send() 71 + .await 72 + .map_err(|e| PaiError::Fetch(format!("Failed to fetch Bluesky feed: {e}")))?; 73 + 74 + if !response.status().is_success() { 75 + return Err(PaiError::Fetch(format!("Bluesky API error: {}", response.status()))); 76 + } 77 + 78 + response 79 + .json::<AuthorFeedResponse>() 80 + .await 81 + .map_err(|e| PaiError::Parse(format!("Failed to parse Bluesky response: {e}"))) 82 + } 83 + 84 + /// Checks if a post is an original post (not a repost or quote) 85 + fn is_original_post(feed_post: &FeedViewPost) -> bool { 86 + feed_post.reason.is_none() 87 + } 88 + 89 + /// Converts an AT URI to a canonical Bluesky URL 90 + /// 91 + /// AT URI format: at://did:plc:xyz/app.bsky.feed.post/abc123 92 + /// URL format: https://bsky.app/profile/{handle}/post/{post_id} 93 + fn at_uri_to_url(uri: &str, handle: &str) -> Result<String> { 94 + let parts: Vec<&str> = uri.split('/').collect(); 95 + if parts.len() >= 4 && parts[0] == "at:" { 96 + let post_id = parts[parts.len() - 1]; 97 + Ok(format!("https://bsky.app/profile/{handle}/post/{post_id}")) 98 + } else { 99 + Err(PaiError::Parse(format!("Invalid AT URI: {uri}"))) 100 + } 101 + } 102 + 103 + /// Extracts text content from the post record 104 + fn extract_text(record: &serde_json::Value) -> Option<String> { 105 + record.get("text").and_then(|v| v.as_str()).map(String::from) 106 + } 107 + 108 + /// Creates a title from the post text (truncated to 100 chars) 109 + fn create_title(text: &str) -> String { 110 + if text.len() <= 100 { 111 + text.to_string() 112 + } else { 113 + format!("{}...", &text[..97]) 114 + } 115 + } 116 + } 117 + 118 + impl SourceFetcher for BlueskyFetcher { 119 + fn sync(&self, storage: &dyn Storage) -> Result<()> { 120 + let runtime = 121 + tokio::runtime::Runtime::new().map_err(|e| PaiError::Fetch(format!("Failed to create runtime: {e}")))?; 122 + 123 + runtime.block_on(async { 124 + let response = self.fetch_author_feed().await?; 125 + 126 + for feed_post in response.feed { 127 + if !Self::is_original_post(&feed_post) { 128 + continue; 129 + } 130 + 131 + let post = feed_post.post; 132 + let text = Self::extract_text(&post.record); 133 + 134 + let title = text.as_ref().map(|t| Self::create_title(t)); 135 + let url = Self::at_uri_to_url(&post.uri, &post.author.handle)?; 136 + 137 + let published_at = post 138 + .record 139 + .get("createdAt") 140 + .and_then(|v| v.as_str()) 141 + .map(String::from) 142 + .unwrap_or_else(|| Utc::now().to_rfc3339()); 143 + 144 + let item = Item { 145 + id: post.uri.clone(), 146 + source_kind: SourceKind::Bluesky, 147 + source_id: self.config.handle.clone(), 148 + author: Some(post.author.handle.clone()), 149 + title, 150 + summary: text, 151 + url, 152 + content_html: None, 153 + published_at, 154 + created_at: Utc::now().to_rfc3339(), 155 + }; 156 + 157 + storage.insert_or_replace_item(&item)?; 158 + } 159 + 160 + Ok(()) 161 + }) 162 + } 163 + } 164 + 165 + #[cfg(test)] 166 + mod tests { 167 + use super::*; 168 + 169 + #[test] 170 + fn at_uri_to_url_valid() { 171 + let uri = "at://did:plc:abc123/app.bsky.feed.post/xyz789"; 172 + let url = BlueskyFetcher::at_uri_to_url(uri, "user.bsky.social").unwrap(); 173 + assert_eq!(url, "https://bsky.app/profile/user.bsky.social/post/xyz789"); 174 + } 175 + 176 + #[test] 177 + fn at_uri_to_url_invalid() { 178 + let uri = "invalid-uri"; 179 + assert!(BlueskyFetcher::at_uri_to_url(uri, "user.bsky.social").is_err()); 180 + } 181 + 182 + #[test] 183 + fn create_title_short_text() { 184 + let text = "Short post"; 185 + assert_eq!(BlueskyFetcher::create_title(text), "Short post"); 186 + } 187 + 188 + #[test] 189 + fn create_title_long_text() { 190 + let text = "This is a very long post that exceeds one hundred characters and should be truncated with ellipsis at the end"; 191 + let title = BlueskyFetcher::create_title(text); 192 + assert!(title.ends_with("...")); 193 + assert_eq!(title.len(), 100); 194 + } 195 + 196 + #[test] 197 + fn extract_text_from_record() { 198 + let record = serde_json::json!({ 199 + "text": "Hello world", 200 + "createdAt": "2024-01-01T12:00:00Z" 201 + }); 202 + let text = BlueskyFetcher::extract_text(&record).unwrap(); 203 + assert_eq!(text, "Hello world"); 204 + } 205 + 206 + #[test] 207 + fn extract_text_missing() { 208 + let record = serde_json::json!({ 209 + "createdAt": "2024-01-01T12:00:00Z" 210 + }); 211 + assert!(BlueskyFetcher::extract_text(&record).is_none()); 212 + } 213 + 214 + #[test] 215 + fn is_original_post_true() { 216 + let feed_post = FeedViewPost { 217 + post: PostView { 218 + uri: "at://test".to_string(), 219 + cid: "cid123".to_string(), 220 + author: Author { 221 + did: "did:plc:test".to_string(), 222 + handle: "test.bsky.social".to_string(), 223 + display_name: None, 224 + }, 225 + record: serde_json::json!({}), 226 + indexed_at: "2024-01-01T12:00:00Z".to_string(), 227 + }, 228 + reason: None, 229 + }; 230 + assert!(BlueskyFetcher::is_original_post(&feed_post)); 231 + } 232 + 233 + #[test] 234 + fn is_original_post_false_repost() { 235 + let feed_post = FeedViewPost { 236 + post: PostView { 237 + uri: "at://test".to_string(), 238 + cid: "cid123".to_string(), 239 + author: Author { 240 + did: "did:plc:test".to_string(), 241 + handle: "test.bsky.social".to_string(), 242 + display_name: None, 243 + }, 244 + record: serde_json::json!({}), 245 + indexed_at: "2024-01-01T12:00:00Z".to_string(), 246 + }, 247 + reason: Some(serde_json::json!({"$type": "app.bsky.feed.defs#reasonRepost"})), 248 + }; 249 + assert!(!BlueskyFetcher::is_original_post(&feed_post)); 250 + } 251 + }
+133
core/src/fetchers/leaflet.rs
··· 1 + use crate::{Item, LeafletConfig, PaiError, Result, SourceFetcher, SourceKind, Storage}; 2 + use chrono::Utc; 3 + use feed_rs::parser; 4 + 5 + /// Fetcher for Leaflet publications via RSS 6 + /// 7 + /// Retrieves posts from Leaflet publications by parsing their RSS feeds. 8 + /// Each Leaflet publication provides an RSS feed at {base_url}/rss. 9 + pub struct LeafletFetcher { 10 + config: LeafletConfig, 11 + client: reqwest::Client, 12 + } 13 + 14 + impl LeafletFetcher { 15 + /// Creates a new Leaflet fetcher with the given configuration 16 + pub fn new(config: LeafletConfig) -> Self { 17 + Self { config, client: reqwest::Client::new() } 18 + } 19 + 20 + /// Fetches and parses the RSS feed 21 + async fn fetch_feed(&self) -> Result<feed_rs::model::Feed> { 22 + let feed_url = format!("{}/rss", self.config.base_url.trim_end_matches('/')); 23 + let response = self 24 + .client 25 + .get(&feed_url) 26 + .send() 27 + .await 28 + .map_err(|e| PaiError::Fetch(format!("Failed to fetch Leaflet RSS feed: {e}")))?; 29 + 30 + let body = response 31 + .text() 32 + .await 33 + .map_err(|e| PaiError::Fetch(format!("Failed to read response body: {e}")))?; 34 + 35 + parser::parse(body.as_bytes()).map_err(|e| PaiError::Parse(format!("Failed to parse RSS feed: {e}"))) 36 + } 37 + } 38 + 39 + impl SourceFetcher for LeafletFetcher { 40 + fn sync(&self, storage: &dyn Storage) -> Result<()> { 41 + let runtime = 42 + tokio::runtime::Runtime::new().map_err(|e| PaiError::Fetch(format!("Failed to create runtime: {e}")))?; 43 + 44 + runtime.block_on(async { 45 + let feed = self.fetch_feed().await?; 46 + 47 + for entry in feed.entries { 48 + let id = entry.id.clone(); 49 + let url = entry 50 + .links 51 + .first() 52 + .map(|link| link.href.clone()) 53 + .unwrap_or_else(|| id.clone()); 54 + 55 + let title = entry.title.as_ref().map(|t| t.content.clone()); 56 + let summary = entry.summary.as_ref().map(|s| s.content.clone()); 57 + let author = entry.authors.first().map(|a| a.name.clone()); 58 + let content_html = entry.content.and_then(|c| c.body); 59 + 60 + let published_at = entry 61 + .published 62 + .or(entry.updated) 63 + .map(|dt| dt.to_rfc3339()) 64 + .unwrap_or_else(|| Utc::now().to_rfc3339()); 65 + 66 + let item = Item { 67 + id, 68 + source_kind: SourceKind::Leaflet, 69 + source_id: self.config.id.clone(), 70 + author, 71 + title, 72 + summary, 73 + url, 74 + content_html, 75 + published_at, 76 + created_at: Utc::now().to_rfc3339(), 77 + }; 78 + 79 + storage.insert_or_replace_item(&item)?; 80 + } 81 + 82 + Ok(()) 83 + }) 84 + } 85 + } 86 + 87 + #[cfg(test)] 88 + mod tests { 89 + use super::*; 90 + 91 + #[test] 92 + fn parse_valid_rss() { 93 + let rss = r#"<?xml version="1.0" encoding="UTF-8"?> 94 + <rss version="2.0"> 95 + <channel> 96 + <title>Test Leaflet</title> 97 + <link>https://test.leaflet.pub</link> 98 + <description>Test publication</description> 99 + <item> 100 + <title>Test Post</title> 101 + <link>https://test.leaflet.pub/test-post</link> 102 + <guid>test-guid</guid> 103 + <pubDate>Mon, 01 Jan 2024 12:00:00 +0000</pubDate> 104 + <description>Test summary</description> 105 + </item> 106 + </channel> 107 + </rss>"#; 108 + 109 + let feed = parser::parse(rss.as_bytes()).unwrap(); 110 + assert_eq!(feed.entries.len(), 1); 111 + assert_eq!(feed.entries[0].title.as_ref().unwrap().content, "Test Post"); 112 + } 113 + 114 + #[test] 115 + fn parse_invalid_rss() { 116 + let invalid_rss = "this is not valid XML"; 117 + let result = parser::parse(invalid_rss.as_bytes()); 118 + assert!(result.is_err()); 119 + } 120 + 121 + #[test] 122 + fn parse_empty_rss() { 123 + let rss = r#"<?xml version="1.0" encoding="UTF-8"?> 124 + <rss version="2.0"> 125 + <channel> 126 + <title>Empty Feed</title> 127 + </channel> 128 + </rss>"#; 129 + 130 + let feed = parser::parse(rss.as_bytes()).unwrap(); 131 + assert_eq!(feed.entries.len(), 0); 132 + } 133 + }
+7
core/src/fetchers/mod.rs
··· 1 + mod bluesky; 2 + mod leaflet; 3 + mod substack; 4 + 5 + pub use bluesky::BlueskyFetcher; 6 + pub use leaflet::LeafletFetcher; 7 + pub use substack::SubstackFetcher;
+188
core/src/fetchers/substack.rs
··· 1 + use crate::{Item, PaiError, Result, SourceFetcher, SourceKind, Storage, SubstackConfig}; 2 + use chrono::Utc; 3 + use feed_rs::parser; 4 + 5 + /// Fetcher for Substack RSS feeds 6 + /// 7 + /// Retrieves posts from a Substack publication by parsing its RSS feed. 8 + /// Maps RSS items to the standardized Item struct for storage. 9 + pub struct SubstackFetcher { 10 + config: SubstackConfig, 11 + client: reqwest::Client, 12 + } 13 + 14 + impl SubstackFetcher { 15 + /// Creates a new Substack fetcher with the given configuration 16 + pub fn new(config: SubstackConfig) -> Self { 17 + Self { config, client: reqwest::Client::new() } 18 + } 19 + 20 + /// Fetches and parses the RSS feed 21 + async fn fetch_feed(&self) -> Result<feed_rs::model::Feed> { 22 + let feed_url = format!("{}/feed", self.config.base_url); 23 + let response = self 24 + .client 25 + .get(&feed_url) 26 + .send() 27 + .await 28 + .map_err(|e| PaiError::Fetch(format!("Failed to fetch RSS feed: {e}")))?; 29 + 30 + let body = response 31 + .text() 32 + .await 33 + .map_err(|e| PaiError::Fetch(format!("Failed to read response body: {e}")))?; 34 + 35 + parser::parse(body.as_bytes()).map_err(|e| PaiError::Parse(format!("Failed to parse RSS feed: {e}"))) 36 + } 37 + 38 + /// Extracts the source ID from the base URL (e.g., "patternmatched.substack.com") 39 + fn extract_source_id(&self) -> String { 40 + self.config 41 + .base_url 42 + .trim_start_matches("https://") 43 + .trim_start_matches("http://") 44 + .trim_end_matches('/') 45 + .to_string() 46 + } 47 + } 48 + 49 + impl SourceFetcher for SubstackFetcher { 50 + fn sync(&self, storage: &dyn Storage) -> Result<()> { 51 + let runtime = 52 + tokio::runtime::Runtime::new().map_err(|e| PaiError::Fetch(format!("Failed to create runtime: {e}")))?; 53 + 54 + runtime.block_on(async { 55 + let feed = self.fetch_feed().await?; 56 + let source_id = self.extract_source_id(); 57 + 58 + for entry in feed.entries { 59 + let id = entry.id.clone(); 60 + let url = entry 61 + .links 62 + .first() 63 + .map(|link| link.href.clone()) 64 + .unwrap_or_else(|| id.clone()); 65 + 66 + let title = entry.title.as_ref().map(|t| t.content.clone()); 67 + let summary = entry.summary.as_ref().map(|s| s.content.clone()); 68 + let author = entry.authors.first().map(|a| a.name.clone()); 69 + let content_html = entry.content.and_then(|c| c.body); 70 + 71 + let published_at = entry 72 + .published 73 + .or(entry.updated) 74 + .map(|dt| dt.to_rfc3339()) 75 + .unwrap_or_else(|| Utc::now().to_rfc3339()); 76 + 77 + let item = Item { 78 + id, 79 + source_kind: SourceKind::Substack, 80 + source_id: source_id.clone(), 81 + author, 82 + title, 83 + summary, 84 + url, 85 + content_html, 86 + published_at, 87 + created_at: Utc::now().to_rfc3339(), 88 + }; 89 + 90 + storage.insert_or_replace_item(&item)?; 91 + } 92 + 93 + Ok(()) 94 + }) 95 + } 96 + } 97 + 98 + #[cfg(test)] 99 + mod tests { 100 + use super::*; 101 + use crate::ListFilter; 102 + use std::sync::{Arc, Mutex}; 103 + 104 + #[derive(Clone)] 105 + #[allow(dead_code)] 106 + struct MockStorage { 107 + items: Arc<Mutex<Vec<Item>>>, 108 + } 109 + 110 + #[allow(dead_code)] 111 + impl MockStorage { 112 + fn new() -> Self { 113 + Self { items: Arc::new(Mutex::new(Vec::new())) } 114 + } 115 + 116 + fn get_items(&self) -> Vec<Item> { 117 + self.items.lock().unwrap().clone() 118 + } 119 + } 120 + 121 + impl Storage for MockStorage { 122 + fn insert_or_replace_item(&self, item: &Item) -> Result<()> { 123 + self.items.lock().unwrap().push(item.clone()); 124 + Ok(()) 125 + } 126 + 127 + fn list_items(&self, _filter: &ListFilter) -> Result<Vec<Item>> { 128 + Ok(self.items.lock().unwrap().clone()) 129 + } 130 + } 131 + 132 + #[test] 133 + fn extract_source_id_https() { 134 + let config = SubstackConfig { enabled: true, base_url: "https://patternmatched.substack.com".to_string() }; 135 + let fetcher = SubstackFetcher::new(config); 136 + assert_eq!(fetcher.extract_source_id(), "patternmatched.substack.com"); 137 + } 138 + 139 + #[test] 140 + fn extract_source_id_http() { 141 + let config = SubstackConfig { enabled: true, base_url: "http://test.substack.com/".to_string() }; 142 + let fetcher = SubstackFetcher::new(config); 143 + assert_eq!(fetcher.extract_source_id(), "test.substack.com"); 144 + } 145 + 146 + #[test] 147 + fn parse_valid_rss() { 148 + let rss = r#"<?xml version="1.0" encoding="UTF-8"?> 149 + <rss version="2.0"> 150 + <channel> 151 + <title>Test Feed</title> 152 + <link>https://test.substack.com</link> 153 + <description>Test</description> 154 + <item> 155 + <title>Test Post</title> 156 + <link>https://test.substack.com/p/test-post</link> 157 + <guid>test-guid</guid> 158 + <pubDate>Mon, 01 Jan 2024 12:00:00 +0000</pubDate> 159 + <description>Test summary</description> 160 + </item> 161 + </channel> 162 + </rss>"#; 163 + 164 + let feed = parser::parse(rss.as_bytes()).unwrap(); 165 + assert_eq!(feed.entries.len(), 1); 166 + assert_eq!(feed.entries[0].title.as_ref().unwrap().content, "Test Post"); 167 + } 168 + 169 + #[test] 170 + fn parse_invalid_rss() { 171 + let invalid_rss = "this is not valid XML"; 172 + let result = parser::parse(invalid_rss.as_bytes()); 173 + assert!(result.is_err()); 174 + } 175 + 176 + #[test] 177 + fn parse_empty_rss() { 178 + let rss = r#"<?xml version="1.0" encoding="UTF-8"?> 179 + <rss version="2.0"> 180 + <channel> 181 + <title>Test Feed</title> 182 + </channel> 183 + </rss>"#; 184 + 185 + let feed = parser::parse(rss.as_bytes()).unwrap(); 186 + assert_eq!(feed.entries.len(), 0); 187 + } 188 + }
+267 -6
core/src/lib.rs
··· 1 - use std::fmt; 1 + mod fetchers; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + use std::path::Path; 5 + use std::{fmt, str::FromStr}; 2 6 use thiserror::Error; 7 + 8 + pub use fetchers::{BlueskyFetcher, LeafletFetcher, SubstackFetcher}; 3 9 4 10 /// Errors that can occur in the Personal Activity Index 5 11 #[derive(Error, Debug)] ··· 26 32 pub type Result<T> = std::result::Result<T, PaiError>; 27 33 28 34 /// Represents the different source types supported by the indexer 29 - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 35 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] 36 + #[serde(rename_all = "lowercase")] 30 37 pub enum SourceKind { 31 38 Substack, 32 39 Bluesky, ··· 111 118 fn sync(&self, storage: &dyn Storage) -> Result<()>; 112 119 } 113 120 121 + /// Configuration for Substack source 122 + #[derive(Debug, Clone, Deserialize, Serialize)] 123 + pub struct SubstackConfig { 124 + #[serde(default)] 125 + pub enabled: bool, 126 + pub base_url: String, 127 + } 128 + 129 + /// Configuration for Bluesky source 130 + #[derive(Debug, Clone, Deserialize, Serialize)] 131 + pub struct BlueskyConfig { 132 + #[serde(default)] 133 + pub enabled: bool, 134 + pub handle: String, 135 + } 136 + 137 + /// Configuration for a single Leaflet publication 138 + #[derive(Debug, Clone, Deserialize, Serialize)] 139 + pub struct LeafletConfig { 140 + #[serde(default)] 141 + pub enabled: bool, 142 + pub id: String, 143 + pub base_url: String, 144 + } 145 + 146 + /// Database configuration 147 + #[derive(Debug, Clone, Deserialize, Serialize, Default)] 148 + pub struct DatabaseConfig { 149 + pub path: Option<String>, 150 + } 151 + 152 + /// Deployment mode configuration 153 + #[derive(Debug, Clone, Deserialize, Serialize, Default)] 154 + pub struct DeploymentConfig { 155 + #[serde(default)] 156 + pub mode: String, 157 + pub cloudflare: Option<CloudflareConfig>, 158 + } 159 + 160 + /// Cloudflare deployment configuration 161 + #[derive(Debug, Clone, Deserialize, Serialize)] 162 + pub struct CloudflareConfig { 163 + pub worker_name: String, 164 + pub d1_binding: String, 165 + pub database_name: String, 166 + } 167 + 168 + /// Sources configuration section 169 + #[derive(Debug, Clone, Deserialize, Serialize, Default)] 170 + pub struct SourcesConfig { 171 + pub substack: Option<SubstackConfig>, 172 + pub bluesky: Option<BlueskyConfig>, 173 + #[serde(default)] 174 + pub leaflet: Vec<LeafletConfig>, 175 + } 176 + 114 177 /// Configuration for all sources 115 - #[derive(Debug, Default)] 116 - pub struct Config {} 178 + #[derive(Debug, Clone, Deserialize, Serialize, Default)] 179 + pub struct Config { 180 + #[serde(default)] 181 + pub database: DatabaseConfig, 182 + #[serde(default)] 183 + pub deployment: DeploymentConfig, 184 + #[serde(default)] 185 + pub sources: SourcesConfig, 186 + } 187 + 188 + impl Config { 189 + /// Load configuration from a TOML file 190 + /// 191 + /// Reads and parses the config file, validating the structure and required fields. 192 + pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> { 193 + let content = 194 + std::fs::read_to_string(path).map_err(|e| PaiError::Config(format!("Failed to read config file: {e}")))?; 195 + Self::from_str(&content) 196 + } 197 + } 198 + 199 + impl FromStr for Config { 200 + type Err = PaiError; 201 + 202 + fn from_str(s: &str) -> Result<Self> { 203 + toml::from_str(s).map_err(|e| PaiError::Config(format!("Failed to parse config: {e}"))) 204 + } 205 + } 117 206 118 207 /// Synchronize all enabled sources 119 208 /// 120 209 /// Calls each configured source fetcher to retrieve and store content. 121 - pub fn sync_all_sources(_config: &Config, _storage: &dyn Storage) -> Result<usize> { 122 - Ok(0) 210 + /// Returns the number of sources successfully synced. 211 + /// 212 + /// Filters sources based on optional kind and source_id parameters. 213 + pub fn sync_all_sources( 214 + config: &Config, storage: &dyn Storage, kind: Option<SourceKind>, source_id: Option<&str>, 215 + ) -> Result<usize> { 216 + let mut synced_count = 0; 217 + 218 + if let Some(ref substack_config) = config.sources.substack { 219 + let should_sync = substack_config.enabled 220 + && match (kind, source_id) { 221 + (Some(k), _) if k != SourceKind::Substack => false, 222 + (_, Some(sid)) => { 223 + let substack_id = substack_config 224 + .base_url 225 + .trim_start_matches("https://") 226 + .trim_start_matches("http://") 227 + .trim_end_matches('/'); 228 + substack_id == sid 229 + } 230 + _ => true, 231 + }; 232 + 233 + if should_sync { 234 + let fetcher = SubstackFetcher::new(substack_config.clone()); 235 + fetcher.sync(storage)?; 236 + synced_count += 1; 237 + } 238 + } 239 + 240 + if let Some(ref bluesky_config) = config.sources.bluesky { 241 + let should_sync = bluesky_config.enabled 242 + && match (kind, source_id) { 243 + (Some(k), _) if k != SourceKind::Bluesky => false, 244 + (_, Some(sid)) => bluesky_config.handle == sid, 245 + _ => true, 246 + }; 247 + 248 + if should_sync { 249 + let fetcher = BlueskyFetcher::new(bluesky_config.clone()); 250 + fetcher.sync(storage)?; 251 + synced_count += 1; 252 + } 253 + } 254 + 255 + for leaflet_config in &config.sources.leaflet { 256 + if !leaflet_config.enabled { 257 + continue; 258 + } 259 + 260 + let should_sync = match (kind, source_id) { 261 + (Some(k), _) if k != SourceKind::Leaflet => false, 262 + (_, Some(sid)) => leaflet_config.id == sid, 263 + _ => true, 264 + }; 265 + 266 + if should_sync { 267 + let fetcher = LeafletFetcher::new(leaflet_config.clone()); 268 + fetcher.sync(storage)?; 269 + synced_count += 1; 270 + } 271 + } 272 + 273 + Ok(synced_count) 123 274 } 124 275 125 276 #[cfg(test)] ··· 156 307 assert!(filter.limit.is_none()); 157 308 assert!(filter.since.is_none()); 158 309 assert!(filter.query.is_none()); 310 + } 311 + 312 + #[test] 313 + fn config_parse_empty() { 314 + let config = Config::from_str("").unwrap(); 315 + assert!(config.sources.substack.is_none()); 316 + assert!(config.sources.bluesky.is_none()); 317 + assert!(config.sources.leaflet.is_empty()); 318 + } 319 + 320 + #[test] 321 + fn config_parse_substack() { 322 + let toml = r#" 323 + [sources.substack] 324 + enabled = true 325 + base_url = "https://patternmatched.substack.com" 326 + "#; 327 + let config = Config::from_str(toml).unwrap(); 328 + let substack = config.sources.substack.as_ref().unwrap(); 329 + assert!(substack.enabled); 330 + assert_eq!(substack.base_url, "https://patternmatched.substack.com"); 331 + } 332 + 333 + #[test] 334 + fn config_parse_bluesky() { 335 + let toml = r#" 336 + [sources.bluesky] 337 + enabled = true 338 + handle = "desertthunder.dev" 339 + "#; 340 + let config = Config::from_str(toml).unwrap(); 341 + let bluesky = config.sources.bluesky.as_ref().unwrap(); 342 + assert!(bluesky.enabled); 343 + assert_eq!(bluesky.handle, "desertthunder.dev"); 344 + } 345 + 346 + #[test] 347 + fn config_parse_leaflet_multiple() { 348 + let toml = r#" 349 + [[sources.leaflet]] 350 + enabled = true 351 + id = "desertthunder" 352 + base_url = "https://desertthunder.leaflet.pub" 353 + 354 + [[sources.leaflet]] 355 + enabled = true 356 + id = "stormlightlabs" 357 + base_url = "https://stormlightlabs.leaflet.pub" 358 + "#; 359 + let config = Config::from_str(toml).unwrap(); 360 + assert_eq!(config.sources.leaflet.len(), 2); 361 + assert_eq!(config.sources.leaflet[0].id, "desertthunder"); 362 + assert_eq!(config.sources.leaflet[1].id, "stormlightlabs"); 363 + } 364 + 365 + #[test] 366 + fn config_parse_all_sources() { 367 + let toml = r#" 368 + [database] 369 + path = "/tmp/test.db" 370 + 371 + [deployment] 372 + mode = "sqlite" 373 + 374 + [sources.substack] 375 + enabled = true 376 + base_url = "https://test.substack.com" 377 + 378 + [sources.bluesky] 379 + enabled = false 380 + handle = "test.bsky.social" 381 + 382 + [[sources.leaflet]] 383 + enabled = true 384 + id = "test" 385 + base_url = "https://test.leaflet.pub" 386 + "#; 387 + let config = Config::from_str(toml).unwrap(); 388 + assert_eq!(config.database.path, Some("/tmp/test.db".to_string())); 389 + assert_eq!(config.deployment.mode, "sqlite"); 390 + assert!(config.sources.substack.is_some()); 391 + assert!(config.sources.bluesky.is_some()); 392 + assert_eq!(config.sources.leaflet.len(), 1); 393 + } 394 + 395 + #[test] 396 + fn config_parse_invalid_toml() { 397 + let toml = "this is not valid toml {{{"; 398 + assert!(Config::from_str(toml).is_err()); 399 + } 400 + 401 + #[test] 402 + fn config_parse_missing_required_field() { 403 + let toml = r#" 404 + [sources.substack] 405 + enabled = true 406 + "#; 407 + let result = Config::from_str(toml); 408 + assert!(result.is_err()); 409 + } 410 + 411 + #[test] 412 + fn config_default_enabled_false() { 413 + let toml = r#" 414 + [sources.substack] 415 + base_url = "https://test.substack.com" 416 + "#; 417 + let config = Config::from_str(toml).unwrap(); 418 + let substack = config.sources.substack.as_ref().unwrap(); 419 + assert!(!substack.enabled); 159 420 } 160 421 }