Learn how to use Rust to build ATProto powered applications

Compare changes

Choose any two refs to compare.

+7
.env.template
··· 1 + # Environment Configuration 2 + PORT="8080" # The port your server will listen on 3 + HOST="127.0.0.1" # Hostname for the server 4 + PUBLIC_URL="" # Set when deployed publicly, e.g. "https://mysite.com". Informs OAuth client id. 5 + # DB_PATH="./statusphere.sqlite3" # The SQLite database path. Leave commented out to use a temporary in-memory database. 6 + 7 +
+2
.gitignore
··· 1 1 /target 2 2 .idea 3 + .env 4 + statusphere.sqlite3
+1061 -129
Cargo.lock
··· 44 44 45 45 [[package]] 46 46 name = "actix-http" 47 - version = "3.9.0" 47 + version = "3.10.0" 48 48 source = "registry+https://github.com/rust-lang/crates.io-index" 49 - checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4" 49 + checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9" 50 50 dependencies = [ 51 51 "actix-codec", 52 52 "actix-rt", 53 53 "actix-service", 54 54 "actix-utils", 55 - "ahash", 56 55 "base64 0.22.1", 57 56 "bitflags", 58 57 "brotli", 59 58 "bytes", 60 59 "bytestring", 61 - "derive_more 0.99.19", 60 + "derive_more 2.0.1", 62 61 "encoding_rs", 63 62 "flate2", 63 + "foldhash", 64 64 "futures-core", 65 65 "h2", 66 66 "http 0.2.12", ··· 72 72 "mime", 73 73 "percent-encoding", 74 74 "pin-project-lite", 75 - "rand", 75 + "rand 0.9.0", 76 76 "sha1", 77 77 "smallvec", 78 78 "tokio", ··· 155 155 "actix-web", 156 156 "anyhow", 157 157 "derive_more 1.0.0", 158 - "rand", 158 + "rand 0.8.5", 159 159 "serde", 160 160 "serde_json", 161 161 "tracing", ··· 173 173 174 174 [[package]] 175 175 name = "actix-web" 176 - version = "4.9.0" 176 + version = "4.10.2" 177 177 source = "registry+https://github.com/rust-lang/crates.io-index" 178 - checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38" 178 + checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d" 179 179 dependencies = [ 180 180 "actix-codec", 181 181 "actix-http", ··· 186 186 "actix-service", 187 187 "actix-utils", 188 188 "actix-web-codegen", 189 - "ahash", 190 189 "bytes", 191 190 "bytestring", 192 191 "cfg-if", 193 192 "cookie", 194 - "derive_more 0.99.19", 193 + "derive_more 2.0.1", 195 194 "encoding_rs", 195 + "foldhash", 196 196 "futures-core", 197 197 "futures-util", 198 198 "impl-more", ··· 210 210 "smallvec", 211 211 "socket2", 212 212 "time", 213 + "tracing", 213 214 "url", 214 215 ] 215 216 ··· 282 283 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" 283 284 dependencies = [ 284 285 "cfg-if", 285 - "getrandom 0.2.15", 286 286 "once_cell", 287 287 "version_check", 288 - "zerocopy", 288 + "zerocopy 0.7.35", 289 289 ] 290 290 291 291 [[package]] ··· 391 391 392 392 [[package]] 393 393 name = "askama" 394 - version = "0.12.1" 394 + version = "0.13.0" 395 395 source = "registry+https://github.com/rust-lang/crates.io-index" 396 - checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28" 396 + checksum = "9a4e46abb203e00ef226442d452769233142bbfdd79c3941e84c8e61c4112543" 397 397 dependencies = [ 398 398 "askama_derive", 399 - "askama_escape", 400 - "humansize", 401 - "num-traits", 399 + "itoa", 402 400 "percent-encoding", 401 + "serde", 402 + "serde_json", 403 403 ] 404 404 405 405 [[package]] 406 406 name = "askama_derive" 407 - version = "0.12.5" 407 + version = "0.13.0" 408 408 source = "registry+https://github.com/rust-lang/crates.io-index" 409 - checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83" 409 + checksum = "54398906821fd32c728135f7b351f0c7494ab95ae421d41b6f5a020e158f28a6" 410 410 dependencies = [ 411 411 "askama_parser", 412 412 "basic-toml", 413 - "mime", 414 - "mime_guess", 413 + "memchr", 415 414 "proc-macro2", 416 415 "quote", 416 + "rustc-hash", 417 417 "serde", 418 + "serde_derive", 418 419 "syn", 419 420 ] 420 421 421 422 [[package]] 422 - name = "askama_escape" 423 - version = "0.10.3" 424 - source = "registry+https://github.com/rust-lang/crates.io-index" 425 - checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341" 426 - 427 - [[package]] 428 423 name = "askama_parser" 429 - version = "0.2.1" 424 + version = "0.13.0" 430 425 source = "registry+https://github.com/rust-lang/crates.io-index" 431 - checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0" 426 + checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" 432 427 dependencies = [ 433 - "nom", 428 + "memchr", 429 + "serde", 430 + "serde_derive", 431 + "winnow", 434 432 ] 435 433 436 434 [[package]] ··· 458 456 ] 459 457 460 458 [[package]] 461 - name = "async-stream" 462 - version = "0.3.6" 459 + name = "async-sqlite" 460 + version = "0.5.0" 463 461 source = "registry+https://github.com/rust-lang/crates.io-index" 464 - checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 462 + checksum = "60659f08ccb3a20c15af150ae736cde366fa0657246be9d194affb0149be188f" 465 463 dependencies = [ 466 - "async-stream-impl", 467 - "futures-core", 468 - "pin-project-lite", 464 + "crossbeam-channel", 465 + "futures-channel", 466 + "futures-util", 467 + "rusqlite", 469 468 ] 470 469 471 470 [[package]] 472 - name = "async-stream-impl" 473 - version = "0.3.6" 471 + name = "async-trait" 472 + version = "0.1.88" 474 473 source = "registry+https://github.com/rust-lang/crates.io-index" 475 - checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 474 + checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" 476 475 dependencies = [ 477 476 "proc-macro2", 478 477 "quote", ··· 481 480 482 481 [[package]] 483 482 name = "atrium-api" 484 - version = "0.25.0" 483 + version = "0.25.2" 485 484 source = "registry+https://github.com/rust-lang/crates.io-index" 486 - checksum = "ea3ea578c768ec91082e424a8d139517b2cb5c75149bf3cec04371a1e74f00f2" 485 + checksum = "0d4eb9b4787aba546015c8ccda1d3924c157cee13d67848997fba74ac8144a07" 487 486 dependencies = [ 488 487 "atrium-common", 489 488 "atrium-xrpc", ··· 502 501 503 502 [[package]] 504 503 name = "atrium-common" 505 - version = "0.1.0" 504 + version = "0.1.1" 506 505 source = "registry+https://github.com/rust-lang/crates.io-index" 507 - checksum = "168e558408847bfed69df1033a32fd051f7a037ebc90ea46e588ccb2bfbd7233" 506 + checksum = "ba30d2f9e1a8b3db8fc97d0a5f91ee5a28f8acdddb771ad74c1b08eda357ca3d" 508 507 dependencies = [ 509 508 "dashmap", 510 509 "lru", ··· 516 515 ] 517 516 518 517 [[package]] 519 - name = "atrium-xrpc" 520 - version = "0.12.1" 518 + name = "atrium-identity" 519 + version = "0.1.3" 521 520 source = "registry+https://github.com/rust-lang/crates.io-index" 522 - checksum = "6b4956d94147cfbb669c68f654eb4fd6a1d00648c810cec79d04ec5425b8f378" 521 + checksum = "007c7fdb0e026c7d01697b78263b2d85742b5113fbc5263f8885280cacceca05" 523 522 dependencies = [ 524 - "http 1.2.0", 523 + "atrium-api", 524 + "atrium-common", 525 + "atrium-xrpc", 525 526 "serde", 526 527 "serde_html_form", 527 528 "serde_json", ··· 530 531 ] 531 532 532 533 [[package]] 533 - name = "atrium-xrpc-client" 534 - version = "0.5.11" 534 + name = "atrium-oauth" 535 + version = "0.1.1" 535 536 source = "registry+https://github.com/rust-lang/crates.io-index" 536 - checksum = "9bab4287ccef501b3892e1325280e61ae79a96eb9ee63dceabc0ed3bea35f2eb" 537 + checksum = "24e59e30ae1aa9bbb99ebf2fa5ca40a8ca6665b6b7e4d1de322d99544045e91e" 537 538 dependencies = [ 539 + "atrium-api", 540 + "atrium-common", 541 + "atrium-identity", 538 542 "atrium-xrpc", 543 + "base64 0.22.1", 544 + "chrono", 545 + "dashmap", 546 + "ecdsa", 547 + "elliptic-curve", 548 + "jose-jwa", 549 + "jose-jwk", 550 + "p256", 551 + "rand 0.8.5", 539 552 "reqwest", 553 + "serde", 554 + "serde_html_form", 555 + "serde_json", 556 + "sha2", 557 + "thiserror", 558 + "tokio", 559 + "trait-variant", 560 + ] 561 + 562 + [[package]] 563 + name = "atrium-xrpc" 564 + version = "0.12.2" 565 + source = "registry+https://github.com/rust-lang/crates.io-index" 566 + checksum = "18a9e526cb2ed3e0a2ca78c3ce2a943d9041a68e067dadf42923b523771e07df" 567 + dependencies = [ 568 + "http 1.2.0", 569 + "serde", 570 + "serde_html_form", 571 + "serde_json", 572 + "thiserror", 573 + "trait-variant", 540 574 ] 541 575 542 576 [[package]] ··· 557 591 "miniz_oxide", 558 592 "object", 559 593 "rustc-demangle", 560 - "windows-targets", 594 + "windows-targets 0.52.6", 561 595 ] 562 596 563 597 [[package]] ··· 567 601 checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" 568 602 569 603 [[package]] 604 + name = "base16ct" 605 + version = "0.2.0" 606 + source = "registry+https://github.com/rust-lang/crates.io-index" 607 + checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" 608 + 609 + [[package]] 570 610 name = "base64" 571 611 version = "0.20.0" 572 612 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 574 614 575 615 [[package]] 576 616 name = "base64" 617 + version = "0.21.7" 618 + source = "registry+https://github.com/rust-lang/crates.io-index" 619 + checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 620 + 621 + [[package]] 622 + name = "base64" 577 623 version = "0.22.1" 578 624 source = "registry+https://github.com/rust-lang/crates.io-index" 579 625 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 580 626 581 627 [[package]] 628 + name = "base64ct" 629 + version = "1.7.3" 630 + source = "registry+https://github.com/rust-lang/crates.io-index" 631 + checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" 632 + 633 + [[package]] 582 634 name = "basic-toml" 583 635 version = "0.1.10" 584 636 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 603 655 ] 604 656 605 657 [[package]] 658 + name = "bon" 659 + version = "3.5.1" 660 + source = "registry+https://github.com/rust-lang/crates.io-index" 661 + checksum = "65268237be94042665b92034f979c42d431d2fd998b49809543afe3e66abad1c" 662 + dependencies = [ 663 + "bon-macros", 664 + "rustversion", 665 + ] 666 + 667 + [[package]] 668 + name = "bon-macros" 669 + version = "3.5.1" 670 + source = "registry+https://github.com/rust-lang/crates.io-index" 671 + checksum = "803c95b2ecf650eb10b5f87dda6b9f6a1b758cee53245e2b7b825c9b3803a443" 672 + dependencies = [ 673 + "darling", 674 + "ident_case", 675 + "prettyplease", 676 + "proc-macro2", 677 + "quote", 678 + "rustversion", 679 + "syn", 680 + ] 681 + 682 + [[package]] 606 683 name = "brotli" 607 - version = "6.0.0" 684 + version = "7.0.0" 608 685 source = "registry+https://github.com/rust-lang/crates.io-index" 609 - checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" 686 + checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" 610 687 dependencies = [ 611 688 "alloc-no-stdlib", 612 689 "alloc-stdlib", ··· 722 799 ] 723 800 724 801 [[package]] 802 + name = "const-oid" 803 + version = "0.9.6" 804 + source = "registry+https://github.com/rust-lang/crates.io-index" 805 + checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 806 + 807 + [[package]] 725 808 name = "convert_case" 726 809 version = "0.4.0" 727 810 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 738 821 "hkdf", 739 822 "hmac", 740 823 "percent-encoding", 741 - "rand", 824 + "rand 0.8.5", 742 825 "sha2", 743 826 "subtle", 744 827 "time", ··· 813 896 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 814 897 815 898 [[package]] 899 + name = "crypto-bigint" 900 + version = "0.5.5" 901 + source = "registry+https://github.com/rust-lang/crates.io-index" 902 + checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" 903 + dependencies = [ 904 + "generic-array", 905 + "rand_core 0.6.4", 906 + "subtle", 907 + "zeroize", 908 + ] 909 + 910 + [[package]] 816 911 name = "crypto-common" 817 912 version = "0.1.6" 818 913 source = "registry+https://github.com/rust-lang/crates.io-index" 819 914 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 820 915 dependencies = [ 821 916 "generic-array", 822 - "rand_core", 917 + "rand_core 0.6.4", 823 918 "typenum", 824 919 ] 825 920 ··· 833 928 ] 834 929 835 930 [[package]] 931 + name = "darling" 932 + version = "0.20.11" 933 + source = "registry+https://github.com/rust-lang/crates.io-index" 934 + checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 935 + dependencies = [ 936 + "darling_core", 937 + "darling_macro", 938 + ] 939 + 940 + [[package]] 941 + name = "darling_core" 942 + version = "0.20.11" 943 + source = "registry+https://github.com/rust-lang/crates.io-index" 944 + checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 945 + dependencies = [ 946 + "fnv", 947 + "ident_case", 948 + "proc-macro2", 949 + "quote", 950 + "strsim", 951 + "syn", 952 + ] 953 + 954 + [[package]] 955 + name = "darling_macro" 956 + version = "0.20.11" 957 + source = "registry+https://github.com/rust-lang/crates.io-index" 958 + checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 959 + dependencies = [ 960 + "darling_core", 961 + "quote", 962 + "syn", 963 + ] 964 + 965 + [[package]] 836 966 name = "dashmap" 837 967 version = "6.1.0" 838 968 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 873 1003 ] 874 1004 875 1005 [[package]] 1006 + name = "der" 1007 + version = "0.7.9" 1008 + source = "registry+https://github.com/rust-lang/crates.io-index" 1009 + checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 1010 + dependencies = [ 1011 + "const-oid", 1012 + "zeroize", 1013 + ] 1014 + 1015 + [[package]] 876 1016 name = "deranged" 877 1017 version = "0.3.11" 878 1018 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 882 1022 ] 883 1023 884 1024 [[package]] 1025 + name = "derive_builder" 1026 + version = "0.20.2" 1027 + source = "registry+https://github.com/rust-lang/crates.io-index" 1028 + checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" 1029 + dependencies = [ 1030 + "derive_builder_macro", 1031 + ] 1032 + 1033 + [[package]] 1034 + name = "derive_builder_core" 1035 + version = "0.20.2" 1036 + source = "registry+https://github.com/rust-lang/crates.io-index" 1037 + checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" 1038 + dependencies = [ 1039 + "darling", 1040 + "proc-macro2", 1041 + "quote", 1042 + "syn", 1043 + ] 1044 + 1045 + [[package]] 1046 + name = "derive_builder_macro" 1047 + version = "0.20.2" 1048 + source = "registry+https://github.com/rust-lang/crates.io-index" 1049 + checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" 1050 + dependencies = [ 1051 + "derive_builder_core", 1052 + "syn", 1053 + ] 1054 + 1055 + [[package]] 885 1056 name = "derive_more" 886 1057 version = "0.99.19" 887 1058 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 900 1071 source = "registry+https://github.com/rust-lang/crates.io-index" 901 1072 checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" 902 1073 dependencies = [ 903 - "derive_more-impl", 1074 + "derive_more-impl 1.0.0", 1075 + ] 1076 + 1077 + [[package]] 1078 + name = "derive_more" 1079 + version = "2.0.1" 1080 + source = "registry+https://github.com/rust-lang/crates.io-index" 1081 + checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" 1082 + dependencies = [ 1083 + "derive_more-impl 2.0.1", 904 1084 ] 905 1085 906 1086 [[package]] ··· 916 1096 ] 917 1097 918 1098 [[package]] 1099 + name = "derive_more-impl" 1100 + version = "2.0.1" 1101 + source = "registry+https://github.com/rust-lang/crates.io-index" 1102 + checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" 1103 + dependencies = [ 1104 + "proc-macro2", 1105 + "quote", 1106 + "syn", 1107 + "unicode-xid", 1108 + ] 1109 + 1110 + [[package]] 919 1111 name = "digest" 920 1112 version = "0.10.7" 921 1113 source = "registry+https://github.com/rust-lang/crates.io-index" 922 1114 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 923 1115 dependencies = [ 924 1116 "block-buffer", 1117 + "const-oid", 925 1118 "crypto-common", 926 1119 "subtle", 927 1120 ] ··· 938 1131 ] 939 1132 940 1133 [[package]] 1134 + name = "dotenv" 1135 + version = "0.15.0" 1136 + source = "registry+https://github.com/rust-lang/crates.io-index" 1137 + checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" 1138 + 1139 + [[package]] 1140 + name = "ecdsa" 1141 + version = "0.16.9" 1142 + source = "registry+https://github.com/rust-lang/crates.io-index" 1143 + checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" 1144 + dependencies = [ 1145 + "der", 1146 + "digest", 1147 + "elliptic-curve", 1148 + "rfc6979", 1149 + "signature", 1150 + ] 1151 + 1152 + [[package]] 1153 + name = "elliptic-curve" 1154 + version = "0.13.8" 1155 + source = "registry+https://github.com/rust-lang/crates.io-index" 1156 + checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" 1157 + dependencies = [ 1158 + "base16ct", 1159 + "crypto-bigint", 1160 + "digest", 1161 + "ff", 1162 + "generic-array", 1163 + "group", 1164 + "rand_core 0.6.4", 1165 + "sec1", 1166 + "subtle", 1167 + "zeroize", 1168 + ] 1169 + 1170 + [[package]] 941 1171 name = "encoding_rs" 942 1172 version = "0.8.35" 943 1173 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 947 1177 ] 948 1178 949 1179 [[package]] 1180 + name = "enum-as-inner" 1181 + version = "0.6.1" 1182 + source = "registry+https://github.com/rust-lang/crates.io-index" 1183 + checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" 1184 + dependencies = [ 1185 + "heck", 1186 + "proc-macro2", 1187 + "quote", 1188 + "syn", 1189 + ] 1190 + 1191 + [[package]] 950 1192 name = "env_filter" 951 1193 version = "0.1.3" 952 1194 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 958 1200 959 1201 [[package]] 960 1202 name = "env_logger" 961 - version = "0.11.6" 1203 + version = "0.11.7" 962 1204 source = "registry+https://github.com/rust-lang/crates.io-index" 963 - checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 1205 + checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" 964 1206 dependencies = [ 965 1207 "anstream", 966 1208 "anstyle", 967 1209 "env_filter", 968 - "humantime", 1210 + "jiff", 969 1211 "log", 970 1212 ] 971 1213 ··· 1007 1249 ] 1008 1250 1009 1251 [[package]] 1252 + name = "fallible-iterator" 1253 + version = "0.3.0" 1254 + source = "registry+https://github.com/rust-lang/crates.io-index" 1255 + checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 1256 + 1257 + [[package]] 1258 + name = "fallible-streaming-iterator" 1259 + version = "0.1.9" 1260 + source = "registry+https://github.com/rust-lang/crates.io-index" 1261 + checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" 1262 + 1263 + [[package]] 1010 1264 name = "fastrand" 1011 1265 version = "2.3.0" 1012 1266 source = "registry+https://github.com/rust-lang/crates.io-index" 1013 1267 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 1014 1268 1015 1269 [[package]] 1270 + name = "ff" 1271 + version = "0.13.1" 1272 + source = "registry+https://github.com/rust-lang/crates.io-index" 1273 + checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" 1274 + dependencies = [ 1275 + "rand_core 0.6.4", 1276 + "subtle", 1277 + ] 1278 + 1279 + [[package]] 1016 1280 name = "flate2" 1017 1281 version = "1.1.0" 1018 1282 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1023 1287 ] 1024 1288 1025 1289 [[package]] 1290 + name = "flume" 1291 + version = "0.11.1" 1292 + source = "registry+https://github.com/rust-lang/crates.io-index" 1293 + checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" 1294 + dependencies = [ 1295 + "futures-core", 1296 + "futures-sink", 1297 + "nanorand", 1298 + "spin", 1299 + ] 1300 + 1301 + [[package]] 1026 1302 name = "fnv" 1027 1303 version = "1.0.7" 1028 1304 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1074 1350 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 1075 1351 1076 1352 [[package]] 1353 + name = "futures-io" 1354 + version = "0.3.31" 1355 + source = "registry+https://github.com/rust-lang/crates.io-index" 1356 + checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 1357 + 1358 + [[package]] 1077 1359 name = "futures-macro" 1078 1360 version = "0.3.31" 1079 1361 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1104 1386 dependencies = [ 1105 1387 "futures-core", 1106 1388 "futures-macro", 1389 + "futures-sink", 1107 1390 "futures-task", 1108 1391 "pin-project-lite", 1109 1392 "pin-utils", ··· 1120 1403 "libc", 1121 1404 "log", 1122 1405 "rustversion", 1123 - "windows", 1406 + "windows 0.58.0", 1124 1407 ] 1125 1408 1126 1409 [[package]] ··· 1131 1414 dependencies = [ 1132 1415 "typenum", 1133 1416 "version_check", 1417 + "zeroize", 1134 1418 ] 1135 1419 1136 1420 [[package]] ··· 1140 1424 checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 1141 1425 dependencies = [ 1142 1426 "cfg-if", 1427 + "js-sys", 1143 1428 "libc", 1144 1429 "wasi 0.11.0+wasi-snapshot-preview1", 1430 + "wasm-bindgen", 1145 1431 ] 1146 1432 1147 1433 [[package]] ··· 1153 1439 "cfg-if", 1154 1440 "libc", 1155 1441 "wasi 0.13.3+wasi-0.2.2", 1156 - "windows-targets", 1442 + "windows-targets 0.52.6", 1157 1443 ] 1158 1444 1159 1445 [[package]] ··· 1173 1459 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 1174 1460 1175 1461 [[package]] 1462 + name = "group" 1463 + version = "0.13.0" 1464 + source = "registry+https://github.com/rust-lang/crates.io-index" 1465 + checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" 1466 + dependencies = [ 1467 + "ff", 1468 + "rand_core 0.6.4", 1469 + "subtle", 1470 + ] 1471 + 1472 + [[package]] 1176 1473 name = "h2" 1177 1474 version = "0.3.26" 1178 1475 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1209 1506 ] 1210 1507 1211 1508 [[package]] 1509 + name = "hashlink" 1510 + version = "0.10.0" 1511 + source = "registry+https://github.com/rust-lang/crates.io-index" 1512 + checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" 1513 + dependencies = [ 1514 + "hashbrown 0.15.2", 1515 + ] 1516 + 1517 + [[package]] 1518 + name = "heck" 1519 + version = "0.5.0" 1520 + source = "registry+https://github.com/rust-lang/crates.io-index" 1521 + checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1522 + 1523 + [[package]] 1524 + name = "hickory-proto" 1525 + version = "0.24.4" 1526 + source = "registry+https://github.com/rust-lang/crates.io-index" 1527 + checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248" 1528 + dependencies = [ 1529 + "async-trait", 1530 + "cfg-if", 1531 + "data-encoding", 1532 + "enum-as-inner", 1533 + "futures-channel", 1534 + "futures-io", 1535 + "futures-util", 1536 + "idna", 1537 + "ipnet", 1538 + "once_cell", 1539 + "rand 0.8.5", 1540 + "thiserror", 1541 + "tinyvec", 1542 + "tokio", 1543 + "tracing", 1544 + "url", 1545 + ] 1546 + 1547 + [[package]] 1548 + name = "hickory-resolver" 1549 + version = "0.24.4" 1550 + source = "registry+https://github.com/rust-lang/crates.io-index" 1551 + checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e" 1552 + dependencies = [ 1553 + "cfg-if", 1554 + "futures-util", 1555 + "hickory-proto", 1556 + "ipconfig", 1557 + "lru-cache", 1558 + "once_cell", 1559 + "parking_lot", 1560 + "rand 0.8.5", 1561 + "resolv-conf", 1562 + "smallvec", 1563 + "thiserror", 1564 + "tokio", 1565 + "tracing", 1566 + ] 1567 + 1568 + [[package]] 1212 1569 name = "hkdf" 1213 1570 version = "0.12.4" 1214 1571 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1224 1581 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 1225 1582 dependencies = [ 1226 1583 "digest", 1584 + ] 1585 + 1586 + [[package]] 1587 + name = "hostname" 1588 + version = "0.4.0" 1589 + source = "registry+https://github.com/rust-lang/crates.io-index" 1590 + checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" 1591 + dependencies = [ 1592 + "cfg-if", 1593 + "libc", 1594 + "windows 0.52.0", 1227 1595 ] 1228 1596 1229 1597 [[package]] ··· 1288 1656 version = "1.0.3" 1289 1657 source = "registry+https://github.com/rust-lang/crates.io-index" 1290 1658 checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 1291 - 1292 - [[package]] 1293 - name = "humansize" 1294 - version = "2.1.3" 1295 - source = "registry+https://github.com/rust-lang/crates.io-index" 1296 - checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" 1297 - dependencies = [ 1298 - "libm", 1299 - ] 1300 - 1301 - [[package]] 1302 - name = "humantime" 1303 - version = "2.1.0" 1304 - source = "registry+https://github.com/rust-lang/crates.io-index" 1305 - checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 1306 1659 1307 1660 [[package]] 1308 1661 name = "hyper" ··· 1500 1853 ] 1501 1854 1502 1855 [[package]] 1856 + name = "ident_case" 1857 + version = "1.0.1" 1858 + source = "registry+https://github.com/rust-lang/crates.io-index" 1859 + checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 1860 + 1861 + [[package]] 1503 1862 name = "idna" 1504 1863 version = "1.0.3" 1505 1864 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1546 1905 ] 1547 1906 1548 1907 [[package]] 1908 + name = "ipconfig" 1909 + version = "0.3.2" 1910 + source = "registry+https://github.com/rust-lang/crates.io-index" 1911 + checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" 1912 + dependencies = [ 1913 + "socket2", 1914 + "widestring", 1915 + "windows-sys 0.48.0", 1916 + "winreg", 1917 + ] 1918 + 1919 + [[package]] 1549 1920 name = "ipld-core" 1550 1921 version = "0.4.2" 1551 1922 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1575 1946 checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1576 1947 1577 1948 [[package]] 1949 + name = "jiff" 1950 + version = "0.2.5" 1951 + source = "registry+https://github.com/rust-lang/crates.io-index" 1952 + checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" 1953 + dependencies = [ 1954 + "jiff-static", 1955 + "log", 1956 + "portable-atomic", 1957 + "portable-atomic-util", 1958 + "serde", 1959 + ] 1960 + 1961 + [[package]] 1962 + name = "jiff-static" 1963 + version = "0.2.5" 1964 + source = "registry+https://github.com/rust-lang/crates.io-index" 1965 + checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" 1966 + dependencies = [ 1967 + "proc-macro2", 1968 + "quote", 1969 + "syn", 1970 + ] 1971 + 1972 + [[package]] 1578 1973 name = "jobserver" 1579 1974 version = "0.1.32" 1580 1975 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1584 1979 ] 1585 1980 1586 1981 [[package]] 1982 + name = "jose-b64" 1983 + version = "0.1.2" 1984 + source = "registry+https://github.com/rust-lang/crates.io-index" 1985 + checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56" 1986 + dependencies = [ 1987 + "base64ct", 1988 + "serde", 1989 + "subtle", 1990 + "zeroize", 1991 + ] 1992 + 1993 + [[package]] 1994 + name = "jose-jwa" 1995 + version = "0.1.2" 1996 + source = "registry+https://github.com/rust-lang/crates.io-index" 1997 + checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7" 1998 + dependencies = [ 1999 + "serde", 2000 + ] 2001 + 2002 + [[package]] 2003 + name = "jose-jwk" 2004 + version = "0.1.2" 2005 + source = "registry+https://github.com/rust-lang/crates.io-index" 2006 + checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7" 2007 + dependencies = [ 2008 + "jose-b64", 2009 + "jose-jwa", 2010 + "p256", 2011 + "serde", 2012 + "zeroize", 2013 + ] 2014 + 2015 + [[package]] 1587 2016 name = "js-sys" 1588 2017 version = "0.3.77" 1589 2018 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1621 2050 checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 1622 2051 1623 2052 [[package]] 1624 - name = "libm" 1625 - version = "0.2.11" 2053 + name = "libsqlite3-sys" 2054 + version = "0.31.0" 1626 2055 source = "registry+https://github.com/rust-lang/crates.io-index" 1627 - checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 2056 + checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4" 2057 + dependencies = [ 2058 + "cc", 2059 + "pkg-config", 2060 + "vcpkg", 2061 + ] 2062 + 2063 + [[package]] 2064 + name = "linked-hash-map" 2065 + version = "0.5.6" 2066 + source = "registry+https://github.com/rust-lang/crates.io-index" 2067 + checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 1628 2068 1629 2069 [[package]] 1630 2070 name = "linux-raw-sys" ··· 1667 2107 1668 2108 [[package]] 1669 2109 name = "log" 1670 - version = "0.4.26" 2110 + version = "0.4.27" 1671 2111 source = "registry+https://github.com/rust-lang/crates.io-index" 1672 - checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 2112 + checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1673 2113 1674 2114 [[package]] 1675 2115 name = "loom" ··· 1694 2134 ] 1695 2135 1696 2136 [[package]] 2137 + name = "lru-cache" 2138 + version = "0.1.2" 2139 + source = "registry+https://github.com/rust-lang/crates.io-index" 2140 + checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" 2141 + dependencies = [ 2142 + "linked-hash-map", 2143 + ] 2144 + 2145 + [[package]] 1697 2146 name = "matchers" 1698 2147 version = "0.1.0" 1699 2148 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1709 2158 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1710 2159 1711 2160 [[package]] 2161 + name = "metrics" 2162 + version = "0.24.1" 2163 + source = "registry+https://github.com/rust-lang/crates.io-index" 2164 + checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3" 2165 + dependencies = [ 2166 + "ahash", 2167 + "portable-atomic", 2168 + ] 2169 + 2170 + [[package]] 1712 2171 name = "mime" 1713 2172 version = "0.3.17" 1714 2173 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1723 2182 "mime", 1724 2183 "unicase", 1725 2184 ] 1726 - 1727 - [[package]] 1728 - name = "minimal-lexical" 1729 - version = "0.2.1" 1730 - source = "registry+https://github.com/rust-lang/crates.io-index" 1731 - checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1732 2185 1733 2186 [[package]] 1734 2187 name = "miniz_oxide" ··· 1796 2249 ] 1797 2250 1798 2251 [[package]] 2252 + name = "nanorand" 2253 + version = "0.7.0" 2254 + source = "registry+https://github.com/rust-lang/crates.io-index" 2255 + checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" 2256 + dependencies = [ 2257 + "getrandom 0.2.15", 2258 + ] 2259 + 2260 + [[package]] 1799 2261 name = "native-tls" 1800 2262 version = "0.2.14" 1801 2263 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1810 2272 "security-framework", 1811 2273 "security-framework-sys", 1812 2274 "tempfile", 1813 - ] 1814 - 1815 - [[package]] 1816 - name = "nom" 1817 - version = "7.1.3" 1818 - source = "registry+https://github.com/rust-lang/crates.io-index" 1819 - checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1820 - dependencies = [ 1821 - "memchr", 1822 - "minimal-lexical", 1823 2275 ] 1824 2276 1825 2277 [[package]] ··· 1919 2371 checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1920 2372 1921 2373 [[package]] 2374 + name = "p256" 2375 + version = "0.13.2" 2376 + source = "registry+https://github.com/rust-lang/crates.io-index" 2377 + checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" 2378 + dependencies = [ 2379 + "ecdsa", 2380 + "elliptic-curve", 2381 + "primeorder", 2382 + "sha2", 2383 + ] 2384 + 2385 + [[package]] 1922 2386 name = "parking" 1923 2387 version = "2.2.1" 1924 2388 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1944 2408 "libc", 1945 2409 "redox_syscall", 1946 2410 "smallvec", 1947 - "windows-targets", 2411 + "windows-targets 0.52.6", 1948 2412 ] 1949 2413 1950 2414 [[package]] ··· 1996 2460 checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" 1997 2461 1998 2462 [[package]] 2463 + name = "portable-atomic-util" 2464 + version = "0.2.4" 2465 + source = "registry+https://github.com/rust-lang/crates.io-index" 2466 + checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 2467 + dependencies = [ 2468 + "portable-atomic", 2469 + ] 2470 + 2471 + [[package]] 1999 2472 name = "powerfmt" 2000 2473 version = "0.2.0" 2001 2474 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2007 2480 source = "registry+https://github.com/rust-lang/crates.io-index" 2008 2481 checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 2009 2482 dependencies = [ 2010 - "zerocopy", 2483 + "zerocopy 0.7.35", 2484 + ] 2485 + 2486 + [[package]] 2487 + name = "prettyplease" 2488 + version = "0.2.31" 2489 + source = "registry+https://github.com/rust-lang/crates.io-index" 2490 + checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" 2491 + dependencies = [ 2492 + "proc-macro2", 2493 + "syn", 2494 + ] 2495 + 2496 + [[package]] 2497 + name = "primeorder" 2498 + version = "0.13.6" 2499 + source = "registry+https://github.com/rust-lang/crates.io-index" 2500 + checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" 2501 + dependencies = [ 2502 + "elliptic-curve", 2011 2503 ] 2012 2504 2013 2505 [[package]] ··· 2035 2527 checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 2036 2528 dependencies = [ 2037 2529 "libc", 2038 - "rand_chacha", 2039 - "rand_core", 2530 + "rand_chacha 0.3.1", 2531 + "rand_core 0.6.4", 2532 + ] 2533 + 2534 + [[package]] 2535 + name = "rand" 2536 + version = "0.9.0" 2537 + source = "registry+https://github.com/rust-lang/crates.io-index" 2538 + checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 2539 + dependencies = [ 2540 + "rand_chacha 0.9.0", 2541 + "rand_core 0.9.3", 2542 + "zerocopy 0.8.24", 2040 2543 ] 2041 2544 2042 2545 [[package]] ··· 2046 2549 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 2047 2550 dependencies = [ 2048 2551 "ppv-lite86", 2049 - "rand_core", 2552 + "rand_core 0.6.4", 2553 + ] 2554 + 2555 + [[package]] 2556 + name = "rand_chacha" 2557 + version = "0.9.0" 2558 + source = "registry+https://github.com/rust-lang/crates.io-index" 2559 + checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 2560 + dependencies = [ 2561 + "ppv-lite86", 2562 + "rand_core 0.9.3", 2050 2563 ] 2051 2564 2052 2565 [[package]] ··· 2056 2569 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 2057 2570 dependencies = [ 2058 2571 "getrandom 0.2.15", 2572 + ] 2573 + 2574 + [[package]] 2575 + name = "rand_core" 2576 + version = "0.9.3" 2577 + source = "registry+https://github.com/rust-lang/crates.io-index" 2578 + checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 2579 + dependencies = [ 2580 + "getrandom 0.3.1", 2059 2581 ] 2060 2582 2061 2583 [[package]] ··· 2142 2664 "once_cell", 2143 2665 "percent-encoding", 2144 2666 "pin-project-lite", 2145 - "rustls-pemfile", 2667 + "rustls-pemfile 2.2.0", 2146 2668 "serde", 2147 2669 "serde_json", 2148 2670 "serde_urlencoded", ··· 2160 2682 ] 2161 2683 2162 2684 [[package]] 2685 + name = "resolv-conf" 2686 + version = "0.7.1" 2687 + source = "registry+https://github.com/rust-lang/crates.io-index" 2688 + checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4" 2689 + dependencies = [ 2690 + "hostname", 2691 + ] 2692 + 2693 + [[package]] 2694 + name = "rfc6979" 2695 + version = "0.4.0" 2696 + source = "registry+https://github.com/rust-lang/crates.io-index" 2697 + checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" 2698 + dependencies = [ 2699 + "hmac", 2700 + "subtle", 2701 + ] 2702 + 2703 + [[package]] 2704 + name = "ring" 2705 + version = "0.17.14" 2706 + source = "registry+https://github.com/rust-lang/crates.io-index" 2707 + checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" 2708 + dependencies = [ 2709 + "cc", 2710 + "cfg-if", 2711 + "getrandom 0.2.15", 2712 + "libc", 2713 + "untrusted", 2714 + "windows-sys 0.52.0", 2715 + ] 2716 + 2717 + [[package]] 2718 + name = "rocketman" 2719 + version = "0.2.0" 2720 + source = "registry+https://github.com/rust-lang/crates.io-index" 2721 + checksum = "4a3aae946adbfdcf80cad8793e02d8eb94be06c925331aa56aeb446795893361" 2722 + dependencies = [ 2723 + "anyhow", 2724 + "async-trait", 2725 + "bon", 2726 + "derive_builder", 2727 + "flume", 2728 + "futures-util", 2729 + "metrics", 2730 + "rand 0.8.5", 2731 + "serde", 2732 + "serde_json", 2733 + "tokio", 2734 + "tokio-tungstenite", 2735 + "tracing", 2736 + "tracing-subscriber", 2737 + "url", 2738 + "zstd", 2739 + ] 2740 + 2741 + [[package]] 2742 + name = "rusqlite" 2743 + version = "0.33.0" 2744 + source = "registry+https://github.com/rust-lang/crates.io-index" 2745 + checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110" 2746 + dependencies = [ 2747 + "bitflags", 2748 + "fallible-iterator", 2749 + "fallible-streaming-iterator", 2750 + "hashlink", 2751 + "libsqlite3-sys", 2752 + "smallvec", 2753 + ] 2754 + 2755 + [[package]] 2163 2756 name = "rustc-demangle" 2164 2757 version = "0.1.24" 2165 2758 source = "registry+https://github.com/rust-lang/crates.io-index" 2166 2759 checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 2167 2760 2168 2761 [[package]] 2762 + name = "rustc-hash" 2763 + version = "2.1.1" 2764 + source = "registry+https://github.com/rust-lang/crates.io-index" 2765 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2766 + 2767 + [[package]] 2169 2768 name = "rustc_version" 2170 2769 version = "0.4.1" 2171 2770 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2188 2787 ] 2189 2788 2190 2789 [[package]] 2790 + name = "rustls" 2791 + version = "0.21.12" 2792 + source = "registry+https://github.com/rust-lang/crates.io-index" 2793 + checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 2794 + dependencies = [ 2795 + "log", 2796 + "ring", 2797 + "rustls-webpki", 2798 + "sct", 2799 + ] 2800 + 2801 + [[package]] 2802 + name = "rustls-native-certs" 2803 + version = "0.6.3" 2804 + source = "registry+https://github.com/rust-lang/crates.io-index" 2805 + checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" 2806 + dependencies = [ 2807 + "openssl-probe", 2808 + "rustls-pemfile 1.0.4", 2809 + "schannel", 2810 + "security-framework", 2811 + ] 2812 + 2813 + [[package]] 2814 + name = "rustls-pemfile" 2815 + version = "1.0.4" 2816 + source = "registry+https://github.com/rust-lang/crates.io-index" 2817 + checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 2818 + dependencies = [ 2819 + "base64 0.21.7", 2820 + ] 2821 + 2822 + [[package]] 2191 2823 name = "rustls-pemfile" 2192 2824 version = "2.2.0" 2193 2825 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2203 2835 checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 2204 2836 2205 2837 [[package]] 2838 + name = "rustls-webpki" 2839 + version = "0.101.7" 2840 + source = "registry+https://github.com/rust-lang/crates.io-index" 2841 + checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 2842 + dependencies = [ 2843 + "ring", 2844 + "untrusted", 2845 + ] 2846 + 2847 + [[package]] 2206 2848 name = "rustversion" 2207 2849 version = "1.0.20" 2208 2850 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2217 2859 "actix-web", 2218 2860 "anyhow", 2219 2861 "askama", 2220 - "async-stream", 2862 + "async-sqlite", 2863 + "async-trait", 2221 2864 "atrium-api", 2222 - "atrium-xrpc-client", 2865 + "atrium-common", 2866 + "atrium-identity", 2867 + "atrium-oauth", 2868 + "chrono", 2869 + "dotenv", 2223 2870 "env_logger", 2871 + "hickory-resolver", 2224 2872 "log", 2873 + "rocketman", 2225 2874 "serde", 2226 2875 "serde_json", 2876 + "thiserror", 2877 + "tokio", 2227 2878 ] 2228 2879 2229 2880 [[package]] ··· 2254 2905 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2255 2906 2256 2907 [[package]] 2908 + name = "sct" 2909 + version = "0.7.1" 2910 + source = "registry+https://github.com/rust-lang/crates.io-index" 2911 + checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 2912 + dependencies = [ 2913 + "ring", 2914 + "untrusted", 2915 + ] 2916 + 2917 + [[package]] 2918 + name = "sec1" 2919 + version = "0.7.3" 2920 + source = "registry+https://github.com/rust-lang/crates.io-index" 2921 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2922 + dependencies = [ 2923 + "base16ct", 2924 + "der", 2925 + "generic-array", 2926 + "subtle", 2927 + "zeroize", 2928 + ] 2929 + 2930 + [[package]] 2257 2931 name = "security-framework" 2258 2932 version = "2.11.1" 2259 2933 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2284 2958 2285 2959 [[package]] 2286 2960 name = "serde" 2287 - version = "1.0.218" 2961 + version = "1.0.219" 2288 2962 source = "registry+https://github.com/rust-lang/crates.io-index" 2289 - checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 2963 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 2290 2964 dependencies = [ 2291 2965 "serde_derive", 2292 2966 ] ··· 2302 2976 2303 2977 [[package]] 2304 2978 name = "serde_derive" 2305 - version = "1.0.218" 2979 + version = "1.0.219" 2306 2980 source = "registry+https://github.com/rust-lang/crates.io-index" 2307 - checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 2981 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 2308 2982 dependencies = [ 2309 2983 "proc-macro2", 2310 2984 "quote", ··· 2395 3069 ] 2396 3070 2397 3071 [[package]] 3072 + name = "signature" 3073 + version = "2.2.0" 3074 + source = "registry+https://github.com/rust-lang/crates.io-index" 3075 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3076 + dependencies = [ 3077 + "digest", 3078 + "rand_core 0.6.4", 3079 + ] 3080 + 3081 + [[package]] 2398 3082 name = "slab" 2399 3083 version = "0.4.9" 2400 3084 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2420 3104 ] 2421 3105 2422 3106 [[package]] 3107 + name = "spin" 3108 + version = "0.9.8" 3109 + source = "registry+https://github.com/rust-lang/crates.io-index" 3110 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3111 + dependencies = [ 3112 + "lock_api", 3113 + ] 3114 + 3115 + [[package]] 2423 3116 name = "stable_deref_trait" 2424 3117 version = "1.2.0" 2425 3118 source = "registry+https://github.com/rust-lang/crates.io-index" 2426 3119 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 3120 + 3121 + [[package]] 3122 + name = "strsim" 3123 + version = "0.11.1" 3124 + source = "registry+https://github.com/rust-lang/crates.io-index" 3125 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2427 3126 2428 3127 [[package]] 2429 3128 name = "subtle" ··· 2554 3253 ] 2555 3254 2556 3255 [[package]] 3256 + name = "tinyvec" 3257 + version = "1.9.0" 3258 + source = "registry+https://github.com/rust-lang/crates.io-index" 3259 + checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 3260 + dependencies = [ 3261 + "tinyvec_macros", 3262 + ] 3263 + 3264 + [[package]] 3265 + name = "tinyvec_macros" 3266 + version = "0.1.1" 3267 + source = "registry+https://github.com/rust-lang/crates.io-index" 3268 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 3269 + 3270 + [[package]] 2557 3271 name = "tokio" 2558 - version = "1.44.0" 3272 + version = "1.44.1" 2559 3273 source = "registry+https://github.com/rust-lang/crates.io-index" 2560 - checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" 3274 + checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 2561 3275 dependencies = [ 2562 3276 "backtrace", 2563 3277 "bytes", ··· 2567 3281 "pin-project-lite", 2568 3282 "signal-hook-registry", 2569 3283 "socket2", 3284 + "tokio-macros", 2570 3285 "windows-sys 0.52.0", 2571 3286 ] 2572 3287 2573 3288 [[package]] 3289 + name = "tokio-macros" 3290 + version = "2.5.0" 3291 + source = "registry+https://github.com/rust-lang/crates.io-index" 3292 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 3293 + dependencies = [ 3294 + "proc-macro2", 3295 + "quote", 3296 + "syn", 3297 + ] 3298 + 3299 + [[package]] 2574 3300 name = "tokio-native-tls" 2575 3301 version = "0.3.1" 2576 3302 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2581 3307 ] 2582 3308 2583 3309 [[package]] 3310 + name = "tokio-rustls" 3311 + version = "0.24.1" 3312 + source = "registry+https://github.com/rust-lang/crates.io-index" 3313 + checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 3314 + dependencies = [ 3315 + "rustls", 3316 + "tokio", 3317 + ] 3318 + 3319 + [[package]] 3320 + name = "tokio-tungstenite" 3321 + version = "0.20.1" 3322 + source = "registry+https://github.com/rust-lang/crates.io-index" 3323 + checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" 3324 + dependencies = [ 3325 + "futures-util", 3326 + "log", 3327 + "rustls", 3328 + "rustls-native-certs", 3329 + "tokio", 3330 + "tokio-rustls", 3331 + "tungstenite", 3332 + "webpki-roots", 3333 + ] 3334 + 3335 + [[package]] 2584 3336 name = "tokio-util" 2585 - version = "0.7.13" 3337 + version = "0.7.14" 2586 3338 source = "registry+https://github.com/rust-lang/crates.io-index" 2587 - checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 3339 + checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 2588 3340 dependencies = [ 2589 3341 "bytes", 2590 3342 "futures-core", ··· 2628 3380 dependencies = [ 2629 3381 "log", 2630 3382 "pin-project-lite", 3383 + "tracing-attributes", 2631 3384 "tracing-core", 2632 3385 ] 2633 3386 2634 3387 [[package]] 3388 + name = "tracing-attributes" 3389 + version = "0.1.28" 3390 + source = "registry+https://github.com/rust-lang/crates.io-index" 3391 + checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 3392 + dependencies = [ 3393 + "proc-macro2", 3394 + "quote", 3395 + "syn", 3396 + ] 3397 + 3398 + [[package]] 2635 3399 name = "tracing-core" 2636 3400 version = "0.1.33" 2637 3401 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2688 3452 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2689 3453 2690 3454 [[package]] 3455 + name = "tungstenite" 3456 + version = "0.20.1" 3457 + source = "registry+https://github.com/rust-lang/crates.io-index" 3458 + checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" 3459 + dependencies = [ 3460 + "byteorder", 3461 + "bytes", 3462 + "data-encoding", 3463 + "http 0.2.12", 3464 + "httparse", 3465 + "log", 3466 + "rand 0.8.5", 3467 + "rustls", 3468 + "sha1", 3469 + "thiserror", 3470 + "url", 3471 + "utf-8", 3472 + ] 3473 + 3474 + [[package]] 2691 3475 name = "typenum" 2692 3476 version = "1.18.0" 2693 3477 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2728 3512 checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 2729 3513 2730 3514 [[package]] 3515 + name = "untrusted" 3516 + version = "0.9.0" 3517 + source = "registry+https://github.com/rust-lang/crates.io-index" 3518 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 3519 + 3520 + [[package]] 2731 3521 name = "url" 2732 3522 version = "2.5.4" 2733 3523 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2737 3527 "idna", 2738 3528 "percent-encoding", 2739 3529 ] 3530 + 3531 + [[package]] 3532 + name = "utf-8" 3533 + version = "0.7.6" 3534 + source = "registry+https://github.com/rust-lang/crates.io-index" 3535 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2740 3536 2741 3537 [[package]] 2742 3538 name = "utf16_iter" ··· 2905 3701 ] 2906 3702 2907 3703 [[package]] 3704 + name = "webpki-roots" 3705 + version = "0.25.4" 3706 + source = "registry+https://github.com/rust-lang/crates.io-index" 3707 + checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" 3708 + 3709 + [[package]] 3710 + name = "widestring" 3711 + version = "1.2.0" 3712 + source = "registry+https://github.com/rust-lang/crates.io-index" 3713 + checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 3714 + 3715 + [[package]] 2908 3716 name = "winapi" 2909 3717 version = "0.3.9" 2910 3718 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2928 3736 2929 3737 [[package]] 2930 3738 name = "windows" 3739 + version = "0.52.0" 3740 + source = "registry+https://github.com/rust-lang/crates.io-index" 3741 + checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 3742 + dependencies = [ 3743 + "windows-core 0.52.0", 3744 + "windows-targets 0.52.6", 3745 + ] 3746 + 3747 + [[package]] 3748 + name = "windows" 2931 3749 version = "0.58.0" 2932 3750 source = "registry+https://github.com/rust-lang/crates.io-index" 2933 3751 checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 2934 3752 dependencies = [ 2935 3753 "windows-core 0.58.0", 2936 - "windows-targets", 3754 + "windows-targets 0.52.6", 2937 3755 ] 2938 3756 2939 3757 [[package]] ··· 2942 3760 source = "registry+https://github.com/rust-lang/crates.io-index" 2943 3761 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2944 3762 dependencies = [ 2945 - "windows-targets", 3763 + "windows-targets 0.52.6", 2946 3764 ] 2947 3765 2948 3766 [[package]] ··· 2955 3773 "windows-interface", 2956 3774 "windows-result", 2957 3775 "windows-strings", 2958 - "windows-targets", 3776 + "windows-targets 0.52.6", 2959 3777 ] 2960 3778 2961 3779 [[package]] ··· 2994 3812 dependencies = [ 2995 3813 "windows-result", 2996 3814 "windows-strings", 2997 - "windows-targets", 3815 + "windows-targets 0.52.6", 2998 3816 ] 2999 3817 3000 3818 [[package]] ··· 3003 3821 source = "registry+https://github.com/rust-lang/crates.io-index" 3004 3822 checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 3005 3823 dependencies = [ 3006 - "windows-targets", 3824 + "windows-targets 0.52.6", 3007 3825 ] 3008 3826 3009 3827 [[package]] ··· 3013 3831 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 3014 3832 dependencies = [ 3015 3833 "windows-result", 3016 - "windows-targets", 3834 + "windows-targets 0.52.6", 3835 + ] 3836 + 3837 + [[package]] 3838 + name = "windows-sys" 3839 + version = "0.48.0" 3840 + source = "registry+https://github.com/rust-lang/crates.io-index" 3841 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3842 + dependencies = [ 3843 + "windows-targets 0.48.5", 3017 3844 ] 3018 3845 3019 3846 [[package]] ··· 3022 3849 source = "registry+https://github.com/rust-lang/crates.io-index" 3023 3850 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3024 3851 dependencies = [ 3025 - "windows-targets", 3852 + "windows-targets 0.52.6", 3026 3853 ] 3027 3854 3028 3855 [[package]] ··· 3031 3858 source = "registry+https://github.com/rust-lang/crates.io-index" 3032 3859 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3033 3860 dependencies = [ 3034 - "windows-targets", 3861 + "windows-targets 0.52.6", 3862 + ] 3863 + 3864 + [[package]] 3865 + name = "windows-targets" 3866 + version = "0.48.5" 3867 + source = "registry+https://github.com/rust-lang/crates.io-index" 3868 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3869 + dependencies = [ 3870 + "windows_aarch64_gnullvm 0.48.5", 3871 + "windows_aarch64_msvc 0.48.5", 3872 + "windows_i686_gnu 0.48.5", 3873 + "windows_i686_msvc 0.48.5", 3874 + "windows_x86_64_gnu 0.48.5", 3875 + "windows_x86_64_gnullvm 0.48.5", 3876 + "windows_x86_64_msvc 0.48.5", 3035 3877 ] 3036 3878 3037 3879 [[package]] ··· 3040 3882 source = "registry+https://github.com/rust-lang/crates.io-index" 3041 3883 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3042 3884 dependencies = [ 3043 - "windows_aarch64_gnullvm", 3044 - "windows_aarch64_msvc", 3045 - "windows_i686_gnu", 3885 + "windows_aarch64_gnullvm 0.52.6", 3886 + "windows_aarch64_msvc 0.52.6", 3887 + "windows_i686_gnu 0.52.6", 3046 3888 "windows_i686_gnullvm", 3047 - "windows_i686_msvc", 3048 - "windows_x86_64_gnu", 3049 - "windows_x86_64_gnullvm", 3050 - "windows_x86_64_msvc", 3889 + "windows_i686_msvc 0.52.6", 3890 + "windows_x86_64_gnu 0.52.6", 3891 + "windows_x86_64_gnullvm 0.52.6", 3892 + "windows_x86_64_msvc 0.52.6", 3051 3893 ] 3052 3894 3053 3895 [[package]] 3054 3896 name = "windows_aarch64_gnullvm" 3897 + version = "0.48.5" 3898 + source = "registry+https://github.com/rust-lang/crates.io-index" 3899 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3900 + 3901 + [[package]] 3902 + name = "windows_aarch64_gnullvm" 3055 3903 version = "0.52.6" 3056 3904 source = "registry+https://github.com/rust-lang/crates.io-index" 3057 3905 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3058 3906 3059 3907 [[package]] 3060 3908 name = "windows_aarch64_msvc" 3909 + version = "0.48.5" 3910 + source = "registry+https://github.com/rust-lang/crates.io-index" 3911 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3912 + 3913 + [[package]] 3914 + name = "windows_aarch64_msvc" 3061 3915 version = "0.52.6" 3062 3916 source = "registry+https://github.com/rust-lang/crates.io-index" 3063 3917 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3918 + 3919 + [[package]] 3920 + name = "windows_i686_gnu" 3921 + version = "0.48.5" 3922 + source = "registry+https://github.com/rust-lang/crates.io-index" 3923 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3064 3924 3065 3925 [[package]] 3066 3926 name = "windows_i686_gnu" ··· 3076 3936 3077 3937 [[package]] 3078 3938 name = "windows_i686_msvc" 3939 + version = "0.48.5" 3940 + source = "registry+https://github.com/rust-lang/crates.io-index" 3941 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3942 + 3943 + [[package]] 3944 + name = "windows_i686_msvc" 3079 3945 version = "0.52.6" 3080 3946 source = "registry+https://github.com/rust-lang/crates.io-index" 3081 3947 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3082 3948 3083 3949 [[package]] 3084 3950 name = "windows_x86_64_gnu" 3951 + version = "0.48.5" 3952 + source = "registry+https://github.com/rust-lang/crates.io-index" 3953 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3954 + 3955 + [[package]] 3956 + name = "windows_x86_64_gnu" 3085 3957 version = "0.52.6" 3086 3958 source = "registry+https://github.com/rust-lang/crates.io-index" 3087 3959 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3088 3960 3089 3961 [[package]] 3090 3962 name = "windows_x86_64_gnullvm" 3963 + version = "0.48.5" 3964 + source = "registry+https://github.com/rust-lang/crates.io-index" 3965 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3966 + 3967 + [[package]] 3968 + name = "windows_x86_64_gnullvm" 3091 3969 version = "0.52.6" 3092 3970 source = "registry+https://github.com/rust-lang/crates.io-index" 3093 3971 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3094 3972 3095 3973 [[package]] 3096 3974 name = "windows_x86_64_msvc" 3975 + version = "0.48.5" 3976 + source = "registry+https://github.com/rust-lang/crates.io-index" 3977 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3978 + 3979 + [[package]] 3980 + name = "windows_x86_64_msvc" 3097 3981 version = "0.52.6" 3098 3982 source = "registry+https://github.com/rust-lang/crates.io-index" 3099 3983 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3100 3984 3101 3985 [[package]] 3986 + name = "winnow" 3987 + version = "0.7.4" 3988 + source = "registry+https://github.com/rust-lang/crates.io-index" 3989 + checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 3990 + dependencies = [ 3991 + "memchr", 3992 + ] 3993 + 3994 + [[package]] 3995 + name = "winreg" 3996 + version = "0.50.0" 3997 + source = "registry+https://github.com/rust-lang/crates.io-index" 3998 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 3999 + dependencies = [ 4000 + "cfg-if", 4001 + "windows-sys 0.48.0", 4002 + ] 4003 + 4004 + [[package]] 3102 4005 name = "wit-bindgen-rt" 3103 4006 version = "0.33.0" 3104 4007 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3150 4053 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 3151 4054 dependencies = [ 3152 4055 "byteorder", 3153 - "zerocopy-derive", 4056 + "zerocopy-derive 0.7.35", 4057 + ] 4058 + 4059 + [[package]] 4060 + name = "zerocopy" 4061 + version = "0.8.24" 4062 + source = "registry+https://github.com/rust-lang/crates.io-index" 4063 + checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 4064 + dependencies = [ 4065 + "zerocopy-derive 0.8.24", 3154 4066 ] 3155 4067 3156 4068 [[package]] ··· 3165 4077 ] 3166 4078 3167 4079 [[package]] 4080 + name = "zerocopy-derive" 4081 + version = "0.8.24" 4082 + source = "registry+https://github.com/rust-lang/crates.io-index" 4083 + checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 4084 + dependencies = [ 4085 + "proc-macro2", 4086 + "quote", 4087 + "syn", 4088 + ] 4089 + 4090 + [[package]] 3168 4091 name = "zerofrom" 3169 4092 version = "0.1.6" 3170 4093 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3183 4106 "quote", 3184 4107 "syn", 3185 4108 "synstructure", 4109 + ] 4110 + 4111 + [[package]] 4112 + name = "zeroize" 4113 + version = "1.8.1" 4114 + source = "registry+https://github.com/rust-lang/crates.io-index" 4115 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 4116 + dependencies = [ 4117 + "serde", 3186 4118 ] 3187 4119 3188 4120 [[package]]
+24 -8
Cargo.toml
··· 4 4 edition = "2024" 5 5 6 6 [dependencies] 7 - askama = "0.12" 8 7 actix-files = "0.6.6" 9 8 actix-session = { version = "0.10", features = ["cookie-session"] } 10 - actix-web = "4.9" 11 - serde = { version = "1.0", features = ["derive"] } 12 - log = "0.4" 13 - async-stream = "0.3" 14 - env_logger = "0.11" 9 + actix-web = "4.10.2" 15 10 anyhow = "1.0.97" 16 - serde_json = "1.0.140" 11 + askama = "0.13" 12 + atrium-common = "0.1.1" 17 13 atrium-api = "0.25.0" 18 - atrium-xrpc-client = "0.5.10" 14 + atrium-identity = "0.1.3" 15 + atrium-oauth = "0.1.0" 16 + chrono = "0.4.40" 17 + env_logger = "0.11.7" 18 + hickory-resolver = "0.24.1" 19 + log = "0.4.27" 20 + serde = { version = "1.0.219", features = ["derive"] } 21 + serde_json = "1.0.140" 22 + rocketman = "0.2.0" 23 + tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 24 + dotenv = "0.15.0" 25 + thiserror = "1.0.69" 26 + async-sqlite = "0.5.0" 27 + async-trait = "0.1.88" 28 + 29 + [build-dependencies] 30 + askama = "0.13" 31 + 32 + 33 + [profile.dev.package.askama_derive] 34 + opt-level = 3
+21
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Bailey Townsend 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+1061 -5
README.md
··· 1 - # Dev commands 1 + # Rusty Statusphere 2 + 3 + Originally taken 4 + from [bluesky-social/atproto-website](https://github.com/bluesky-social/atproto-website/blob/dbcd70ced53078579c7e5b015a26db295b7a7807/src/app/%5Blocale%5D/guides/applications/en.mdx) 5 + 6 + > [!NOTE] 7 + > ***This tutorial is based off of the original quick start guide found [here](https://atproto.com/guides/applications). 8 + > The goal is to follow as closely to the original as possible, expect for one small change. It's in Rust ๐Ÿฆ€. 9 + > All credit goes to the maintainers of the original project and tutorial. This was made to help you get started with 10 + > using Rust to write applications in the Atmosphere. Parts that stray from the tutorial, or need extra context will be in blocks like this one.*** 11 + 12 + # Quick start guide to building applications on AT Protocol 13 + 14 + [Find the source code on GitHub](https://github.com/fatfingers23/rusty_statusphere_example_app) 15 + 16 + In this guide, we're going to build a simple multi-user app that publishes your current "status" as an emoji. Our 17 + application will look like this: 18 + 19 + ![Picture of the application](./images/cover.png) 20 + 21 + We will cover how to: 22 + 23 + - Signin via OAuth 24 + - Fetch information about users (profiles) 25 + - Listen to the network firehose for new data via the [Jetstream](https://docs.bsky.app/blog/jetstream) 26 + - Publish data on the user's account using a custom schema 27 + 28 + We're going to keep this light so you can quickly wrap your head around ATProto. There will be links with more 29 + information about each step. 30 + 31 + ## Introduction 32 + 33 + Data in the Atmosphere is stored on users' personal repos. It's almost like each user has their own website. Our goal is 34 + to aggregate data from the users into our SQLite DB. 35 + 36 + Think of our app like a Google. If Google's job was to say which emoji each website had under `/status.json`, then it 37 + would show something like: 38 + 39 + - `nytimes.com` is feeling ๐Ÿ“ฐ according to `https://nytimes.com/status.json` 40 + - `bsky.app` is feeling ๐Ÿฆ‹ according to `https://bsky.app/status.json` 41 + - `reddit.com` is feeling ๐Ÿค“ according to `https://reddit.com/status.json` 42 + 43 + The Atmosphere works the same way, except we're going to check `at://` instead of `https://`. Each user has a data repo 44 + under an `at://` URL. We'll crawl all the user data repos in the Atmosphere for all the "status.json" records and 45 + aggregate them into our SQLite database. 46 + 47 + > `at://` is the URL scheme of the AT Protocol. Under the hood it uses common tech like HTTP and DNS, but it adds all of 48 + > the features we'll be using in this tutorial. 49 + 50 + ## Step 1. Starting with our Actix Web app 51 + 52 + Start by cloning the repo and installing packages. 53 + 54 + ```bash 55 + git clone https://github.com/fatfingers23/rusty_statusphere_example_app.git 56 + cd rusty_statusphere_example_app 57 + cp .env.template .env 58 + cargo run 59 + # Navigate to http://127.0.0.1:8080 60 + ``` 61 + 62 + Our repo is a regular Web app. We're rendering our HTML server-side like it's 1999. We also have a SQLite database that 63 + we're managing with [async-sqlite](https://crates.io/crates/async-sqlite). 64 + 65 + Our starting stack: 66 + 67 + - [Rust](https://www.rust-lang.org/tools/install) 68 + - Rust web server ([Actix Web](https://actix.rs/)) 69 + - SQLite database ([async-sqlite](https://crates.io/crates/async-sqlite)) 70 + - HTML Templating ([askama](https://crates.io/crates/askama)) 71 + 72 + > [!NOTE] 73 + > Along with the above, we are also using a couple of community maintained projects for using rust with the ATProtocol. 74 + > Since these are community maintained I have also linked sponsor links for the maintainers and _highly_ recommend you to 75 + > think 76 + > about sponsoring them. 77 + > Thanks to their work and projects, we are able to create Rust applications in the Atmosphere. 78 + > - ATProtocol client and OAuth 79 + with [atrium](https://github.com/atrium-rs/atrium) - [sponsor sugyan](https://github.com/sponsors/sugyan) 80 + > - Jetstream consumer 81 + with [rocketman](https://crates.io/crates/rocketman)- [buy natalie a coffee](https://ko-fi.com/uxieq) 82 + 83 + With each step we'll explain how our Web app taps into the Atmosphere. Refer to the codebase for more detailed code 84 + &mdash; again, this tutorial is going to keep it light and quick to digest. 85 + 86 + ## Step 2. Signing in with OAuth 87 + 88 + When somebody logs into our app, they'll give us read & write access to their personal `at://` repo. We'll use that to 89 + write the status json record. 90 + 91 + We're going to accomplish this using OAuth ([spec](https://github.com/bluesky-social/proposals/tree/main/0004-oauth)). 92 + Most of the OAuth flows are going to be handled for us using 93 + the [atrium-oauth](https://crates.io/crates/atrium-oauth) 94 + crate. This is the arrangement we're aiming toward: 95 + 96 + ![A diagram of the OAuth elements](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-oauth.5ebec062.png&w=750&q=75) 97 + 98 + When the user logs in, the OAuth client will create a new session with their repo server and give us read/write access 99 + along with basic user info. 100 + 101 + ![A screenshot of the login UI](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp-login.83cd693f.png&w=640&q=75) 102 + 103 + Our login page just asks the user for their "handle," which is the domain name associated with their account. 104 + For [Bluesky](https://bsky.app) users, these tend to look like `alice.bsky.social`, but they can be any kind of domain ( 105 + eg `alice.com`). 106 + 107 + ```html 108 + <!-- templates/login.html --> 109 + <form action="/login" method="post" class="login-form"> 110 + <input 111 + type="text" 112 + name="handle" 113 + placeholder="Enter your handle (eg alice.bsky.social)" 114 + required 115 + /> 116 + <button type="submit">Log in</button> 117 + </form> 118 + ``` 119 + 120 + When they submit the form, we tell our OAuth client to initiate the authorization flow and then redirect the user to 121 + their server to complete the process. 122 + 123 + ```rust 124 + /** ./src/main.rs **/ 125 + /// Login endpoint 126 + #[post("/login")] 127 + async fn login_post( 128 + request: HttpRequest, 129 + params: web::Form<LoginForm>, 130 + oauth_client: web::Data<OAuthClientType>, 131 + ) -> HttpResponse { 132 + // This will act the same as the js method isValidHandle 133 + match atrium_api::types::string::Handle::new(params.handle.clone()) { 134 + Ok(handle) => { 135 + // Initiates the OAuth flow 136 + let oauth_url = oauth_client 137 + .authorize( 138 + &handle, 139 + AuthorizeOptions { 140 + scopes: vec![ 141 + Scope::Known(KnownScope::Atproto), 142 + Scope::Known(KnownScope::TransitionGeneric), 143 + ], 144 + ..Default::default() 145 + }, 146 + ) 147 + .await; 148 + match oauth_url { 149 + Ok(url) => Redirect::to(url) 150 + .see_other() 151 + .respond_to(&request) 152 + .map_into_boxed_body(), 153 + Err(err) => { 154 + log::error!("Error: {err}"); 155 + let html = LoginTemplate { 156 + title: "Log in", 157 + error: Some("OAuth error"), 158 + }; 159 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 160 + } 161 + } 162 + } 163 + Err(err) => { 164 + let html: LoginTemplate<'_> = LoginTemplate { 165 + title: "Log in", 166 + error: Some(err), 167 + }; 168 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 169 + } 170 + } 171 + } 172 + ``` 173 + 174 + This is the same kind of SSO flow that Google or GitHub uses. The user will be asked for their password, then asked to 175 + confirm the session with your application. 176 + 177 + When that finishes, the user will be sent back to `/oauth/callback` on our Web app. The OAuth client will store the 178 + access tokens for the user's server, and then we attach their account's [DID](https://atproto.com/specs/did) to the 179 + cookie-session. 180 + 181 + ```rust 182 + /** ./src/main.rs **/ 183 + /// OAuth callback endpoint to complete session creation 184 + #[get("/oauth/callback")] 185 + async fn oauth_callback( 186 + request: HttpRequest, 187 + params: web::Query<CallbackParams>, 188 + oauth_client: web::Data<OAuthClientType>, 189 + session: Session, 190 + ) -> HttpResponse { 191 + // Store the credentials 192 + match oauth_client.callback(params.into_inner()).await { 193 + Ok((bsky_session, _)) => { 194 + let agent = Agent::new(bsky_session); 195 + match agent.did().await { 196 + Some(did) => { 197 + //Attach the account DID to our user via a cookie 198 + session.insert("did", did).unwrap(); 199 + Redirect::to("/") 200 + .see_other() 201 + .respond_to(&request) 202 + .map_into_boxed_body() 203 + } 204 + None => { 205 + let html = ErrorTemplate { 206 + title: "Log in", 207 + error: "The OAuth agent did not return a DID. My try relogging in.", 208 + }; 209 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 210 + } 211 + } 212 + } 213 + Err(err) => { 214 + log::error!("Error: {err}"); 215 + let html = ErrorTemplate { 216 + title: "Log in", 217 + error: "OAuth error, check the logs", 218 + }; 219 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 220 + } 221 + } 222 + } 223 + ``` 224 + 225 + With that, we're in business! We now have a session with the user's repo server and can use that to access their data. 226 + 227 + ## Step 3. Fetching the user's profile 228 + 229 + Why don't we learn something about our user? In [Bluesky](https://bsky.app), users publish a "profile" record which 230 + looks like this: 231 + 232 + ```rust 233 + pub struct ProfileViewDetailedData { 234 + pub display_name: Option<String>, // a human friendly name 235 + pub description: Option<String>, // a short bio 236 + pub avatar: Option<String>, // small profile picture 237 + pub banner: Option<String>, // banner image to put on profiles 238 + pub created_at: Option<String> // declared time this profile data was added 239 + // ... 240 + } 241 + ``` 242 + 243 + You can examine this record directly using [atproto-browser.vercel.app](https://atproto-browser.vercel.app). For 244 + instance, [this is the profile record for @bsky.app](https://atproto-browser.vercel.app/at?u=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.actor.profile/self). 245 + 246 + > [!NOTE] 247 + > In the original tutorial `agent.com.atproto.repo.getRecord` is used, which is 248 + > this [method](https://docs.rs/atrium-api/latest/atrium_api/com/atproto/repo/get_record/index.html) in atrium-api. 249 + > For simplicity we are 250 + > using [agent.api.app.bsky.actor.get_profile](https://docs.rs/atrium-api/latest/atrium_api/app/bsky/actor/get_profile/index.html). 251 + > The original text found here has been moved to [Step 4. Reading & writing records](#step-4-reading--writing-records) 252 + > since it makes more sense in that context. 253 + 254 + We're going to use the [Agent](https://crates.io/crates/atrium-oauth) associated with the 255 + user's OAuth session to fetch this record. 256 + 257 + Let's update our homepage to fetch this profile record: 258 + 259 + ```rust 260 + /** ./src/main.rs **/ 261 + /// Homepage 262 + #[get("/")] 263 + async fn home( 264 + _req: HttpRequest, 265 + session: Session, 266 + oauth_client: web::Data<OAuthClientType>, 267 + db_pool: web::Data<Pool>, 268 + handle_resolver: web::Data<HandleResolver>, 269 + ) -> Result<impl Responder> { 270 + const TITLE: &str = "Home"; 271 + 272 + // If the user is signed in, get an agent which communicates with their server 273 + match session.get::<String>("did").unwrap_or(None) { 274 + Some(did) => { 275 + let did = Did::new(did).expect("failed to parse did"); 276 + match oauth_client.restore(&did).await { 277 + Ok(session) => { 278 + let agent = Agent::new(session); 279 + 280 + // Fetch additional information about the logged-in user 281 + let profile = agent 282 + .api 283 + .app 284 + .bsky 285 + .actor 286 + .get_profile( 287 + atrium_api::app::bsky::actor::get_profile::ParametersData { 288 + actor: atrium_api::types::string::AtIdentifier::Did(did), 289 + }.into(), 290 + ) 291 + .await; 292 + 293 + // Serve the logged-in view 294 + let html = HomeTemplate { 295 + title: TITLE, 296 + status_options: &STATUS_OPTIONS, 297 + profile: match profile { 298 + Ok(profile) => { 299 + let profile_data = Profile { 300 + did: profile.did.to_string(), 301 + display_name: profile.display_name.clone(), 302 + }; 303 + Some(profile_data) 304 + } 305 + Err(err) => { 306 + log::error!("Error accessing profile: {err}"); 307 + None 308 + } 309 + }, 310 + }.render().expect("template should be valid"); 311 + 312 + Ok(web::Html::new(html)) 313 + } 314 + Err(err) => { 315 + //Unset the session 316 + session.remove("did"); 317 + log::error!("Error restoring session: {err}"); 318 + let error_html = ErrorTemplate { 319 + title: TITLE, 320 + error: "Was an error resuming the session, please check the logs.", 321 + }.render().expect("template should be valid"); 322 + 323 + Ok(web::Html::new(error_html)) 324 + } 325 + } 326 + } 327 + None => { 328 + // Serve the logged-out view 329 + let html = HomeTemplate { 330 + title: TITLE, 331 + status_options: &STATUS_OPTIONS, 332 + profile: None, 333 + }.render().expect("template should be valid"); 334 + 335 + Ok(web::Html::new(html)) 336 + } 337 + } 338 + } 339 + ``` 2 340 3 - tailwinds 4 - watchexec -w templates -r ~/Applications/tailwindcss --input public/css/base.css --output public/css/style.css -m 341 + With that data, we can give a nice personalized welcome banner for our user: 342 + 343 + ![A screenshot of the banner image](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp-banner.1e92c654.png&w=640&q=75) 5 344 6 - watch actix 7 - watchexec -w templates -w src -r cargo run 345 + ```html 346 + <!-- templates/home.html --> 347 + <div class="card"> 348 + {% if let Some(Profile {did, display_name}) = profile %} 349 + <form action="/logout" method="post" class="session-form"> 350 + <div> 351 + Hi, 352 + {% if let Some(display_name) = display_name %} 353 + <strong>{{display_name}}</strong> 354 + {% else %} 355 + <strong>friend</strong> 356 + {% endif %}. 357 + What's your status today?? 358 + </div> 359 + <div> 360 + <button type="submit">Log out</button> 361 + </div> 362 + </form> 363 + {% else %} 364 + <div class="session-form"> 365 + <div><a href="/login">Log in</a> to set your status!</div> 366 + <div> 367 + <a href="/login" class="button">Log in</a> 368 + </div> 369 + </div> 370 + {% endif %} 371 + </div> 372 + ``` 373 + 374 + ## Step 4. Reading & writing records 375 + 376 + You can think of the user repositories as collections of JSON records: 377 + 378 + !["A diagram of a repository"](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-repo.4a34005b.png&w=750&q=75) 379 + 380 + When asking for a record, we provide three pieces of information. 381 + 382 + - **repo** The [DID](https://atproto.com/specs/did) which identifies the user, 383 + - **collection** The collection name, and 384 + - **rkey** The record key 385 + 386 + We'll explain the collection name shortly. Record keys are strings 387 + with [some restrictions](https://atproto.com/specs/record-key#record-key-syntax) and a couple of common patterns. The 388 + `"self"` pattern is used when a collection is expected to only contain one record which describes the user. 389 + 390 + Let's look again at how we read the "profile" record: 391 + 392 + ```rust 393 + fn example_get_record() { 394 + let get_result = agent 395 + .api 396 + .com 397 + .atproto 398 + .repo 399 + .get_record( 400 + atrium_api::com::atproto::repo::get_record::ParametersData { 401 + cid: None, 402 + collection: "app.bsky.actor.profile" // The collection 403 + .parse() 404 + .unwrap(), 405 + repo: did.into(), // The user 406 + rkey: "self".parse().unwrap(), // The record key 407 + } 408 + .into(), 409 + ) 410 + .await; 411 + } 412 + 413 + ``` 414 + 415 + We write records using a similar API. Since our goal is to write "status" records, let's look at how that will happen: 416 + 417 + ```rust 418 + fn example_create_record() { 419 + let did = atrium_api::types::string::Did::new(did_string.clone()).unwrap(); 420 + let agent = Agent::new(session); 421 + 422 + let status: Unknown = serde_json::from_str( 423 + format!( 424 + r#"{{"$type":"xyz.statusphere.status","status":"{}","createdAt":"{}"}}"#, 425 + form.status, 426 + Datetime::now().as_str() 427 + ) 428 + .as_str(), 429 + ).unwrap(); 430 + 431 + let create_result = agent 432 + .api 433 + .com 434 + .atproto 435 + .repo 436 + .create_record( 437 + atrium_api::com::atproto::repo::create_record::InputData { 438 + collection: Status::NSID.parse().unwrap(), // The collection 439 + repo: did.clone().into(), // The user 440 + rkey: None, // The record key, auto creates with None 441 + record: status, // The record from a strong type 442 + swap_commit: None, 443 + validate: None, 444 + } 445 + .into(), 446 + ) 447 + .await; 448 + } 449 + ``` 450 + 451 + Our `POST /status` route is going to use this API to publish the user's status to their repo. 452 + 453 + ```rust 454 + /// "Set status" Endpoint 455 + #[post("/status")] 456 + async fn status( 457 + request: HttpRequest, 458 + session: Session, 459 + oauth_client: web::Data<OAuthClientType>, 460 + db_pool: web::Data<Pool>, 461 + form: web::Form<StatusForm>, 462 + ) -> HttpResponse { 463 + const TITLE: &str = "Home"; 464 + 465 + // If the user is signed in, get an agent which communicates with their server 466 + match session.get::<String>("did").unwrap_or(None) { 467 + Some(did_string) => { 468 + let did = atrium_api::types::string::Did::new(did_string.clone()) 469 + .expect("failed to parse did"); 470 + match oauth_client.restore(&did).await { 471 + Ok(session) => { 472 + let agent = Agent::new(session); 473 + 474 + // Construct their status record 475 + let status: Unknown = serde_json::from_str( 476 + format!( 477 + r#"{{"$type":"xyz.statusphere.status","status":"{}","createdAt":"{}"}}"#, 478 + form.status, 479 + Datetime::now().as_str() 480 + ) 481 + .as_str(), 482 + ).unwrap(); 483 + 484 + // Write the status record to the user's repository 485 + let create_result = agent 486 + .api 487 + .com 488 + .atproto 489 + .repo 490 + .create_record( 491 + atrium_api::com::atproto::repo::create_record::InputData { 492 + collection: "xyz.statusphere.status".parse().unwrap(), 493 + repo: did.clone().into(), 494 + rkey: None, 495 + record: status, 496 + swap_commit: None, 497 + validate: None, 498 + } 499 + .into(), 500 + ) 501 + .await; 502 + 503 + match create_result { 504 + Ok(_) => Redirect::to("/") 505 + .see_other() 506 + .respond_to(&request) 507 + .map_into_boxed_body(), 508 + Err(err) => { 509 + log::error!("Error creating status: {err}"); 510 + let error_html = ErrorTemplate { 511 + title: TITLE, 512 + error: "Was an error creating the status, please check the logs.", 513 + } 514 + .render() 515 + .expect("template should be valid"); 516 + HttpResponse::Ok().body(error_html) 517 + } 518 + } 519 + } 520 + Err(err) => { 521 + //Unset the session 522 + session.remove("did"); 523 + log::error!( 524 + "Error restoring session, we are removing the session from the cookie: {err}" 525 + ); 526 + let error_html = ErrorTemplate { 527 + title: TITLE, 528 + error: "Was an error resuming the session, please check the logs.", 529 + } 530 + .render() 531 + .expect("template should be valid"); 532 + HttpResponse::Ok().body(error_html) 533 + } 534 + } 535 + } 536 + None => { 537 + let error_template = ErrorTemplate { 538 + title: "Error", 539 + error: "You must be logged in to create a status.", 540 + } 541 + .render() 542 + .expect("template should be valid"); 543 + HttpResponse::Ok().body(error_template) 544 + } 545 + } 546 + } 547 + ``` 548 + 549 + Now in our homepage we can list out the status buttons: 550 + 551 + ```html 552 + <!-- templates/home.html --> 553 + <form action="/status" method="post" class="status-options"> 554 + {% for status in status_options %} 555 + <button 556 + class="{% if let Some(my_status) = my_status %} {%if my_status == status %} status-option selected {% else %} status-option {% endif %} {% else %} status-option {%endif%} " 557 + name="status" value="{{status}}"> 558 + {{status}} 559 + </button> 560 + {% endfor %} 561 + </form> 562 + ``` 563 + 564 + And here we are! 565 + 566 + ![A screenshot of the app's status options"](./images/emojis.png) 567 + 568 + ## Step 5. Creating a custom "status" schema 569 + 570 + Repo collections are typed, meaning that they have a defined schema. The `app.bsky.actor.profile` type 571 + definition [can be found here](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/profile.json). 572 + 573 + Anybody can create a new schema using the [Lexicon](https://atproto.com/specs/lexicon) language, which is very similar 574 + to [JSON-Schema](http://json-schema.org/). The schemas use [reverse-DNS IDs](https://atproto.com/specs/nsid) which 575 + indicate ownership. In this demo app we're going to use `xyz.statusphere` which we registered specifically for this 576 + project (aka statusphere.xyz). 577 + 578 + > ### Why create a schema? 579 + > 580 + > Schemas help other applications understand the data your app is creating. By publishing your schemas, you make it 581 + > easier for other application authors to publish data in a format your app will recognize and handle. 582 + 583 + Let's create our schema in the `/lexicons` folder of our codebase. You 584 + can [read more about how to define schemas here](https://atproto.com/guides/lexicon). 585 + 586 + ```json 587 + /** lexicons/status.json **/ 588 + { 589 + "lexicon": 1, 590 + "id": "xyz.statusphere.status", 591 + "defs": { 592 + "main": { 593 + "type": "record", 594 + "key": "tid", 595 + "record": { 596 + "type": "object", 597 + "required": [ 598 + "status", 599 + "createdAt" 600 + ], 601 + "properties": { 602 + "status": { 603 + "type": "string", 604 + "minLength": 1, 605 + "maxGraphemes": 1, 606 + "maxLength": 32 607 + }, 608 + "createdAt": { 609 + "type": "string", 610 + "format": "datetime" 611 + } 612 + } 613 + } 614 + } 615 + } 616 + } 617 + ``` 618 + 619 + Now let's run some code-generation using our schema: 620 + 621 + > [!NOTE] 622 + > For generating schemas, we are going to 623 + > use [esquema-cli](https://github.com/fatfingers23/esquema?tab=readme-ov-file) 624 + > (Which is a tool I've created from a fork of atrium's codegen). 625 + > This can be installed by running this command 626 + `cargo install esquema-cli --git https://github.com/fatfingers23/esquema.git` 627 + > This is a WIP tool with bugs and missing features. But it's good enough for us to generate Rust types from the lexicon 628 + > schema. 629 + 630 + ```bash 631 + esquema-cli generate local -l ./lexicons/ -o ./src/ --module lexicons 632 + ``` 633 + 634 + 635 + 636 + This will produce Rust structs. Here's what that generated code looks like: 637 + 638 + ```rust 639 + /** ./src/lexicons/xyz/statusphere/status.rs **/ 640 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 641 + //!Definitions for the `xyz.statusphere.status` namespace. 642 + use atrium_api::types::TryFromUnknown; 643 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 644 + #[serde(rename_all = "camelCase")] 645 + pub struct RecordData { 646 + pub created_at: atrium_api::types::string::Datetime, 647 + pub status: String, 648 + } 649 + pub type Record = atrium_api::types::Object<RecordData>; 650 + impl From<atrium_api::types::Unknown> for RecordData { 651 + fn from(value: atrium_api::types::Unknown) -> Self { 652 + Self::try_from_unknown(value).unwrap() 653 + } 654 + } 655 + 656 + ``` 657 + 658 + > [!NOTE] 659 + > You may have noticed we do not cover the validation part like in the TypeScript version. 660 + > Esquema can validate to a point such as the data structure and if a field is there or not. 661 + > But validation of the data itself is not possible, yet. 662 + > There are plans to add it. 663 + > Maybe you would like to add it? 664 + > https://github.com/fatfingers23/esquema/issues/3 665 + 666 + Let's use that code to improve the `POST /status` route: 667 + 668 + ```rust 669 + /// "Set status" Endpoint 670 + #[post("/status")] 671 + async fn status( 672 + request: HttpRequest, 673 + session: Session, 674 + oauth_client: web::Data<OAuthClientType>, 675 + db_pool: web::Data<Pool>, 676 + form: web::Form<StatusForm>, 677 + ) -> HttpResponse { 678 + // ... 679 + let agent = Agent::new(session); 680 + //We use the new status type we generated with esquema 681 + let status: KnownRecord = lexicons::xyz::statusphere::status::RecordData { 682 + created_at: Datetime::now(), 683 + status: form.status.clone(), 684 + } 685 + .into(); 686 + 687 + // TODO no validation yet from esquema 688 + // Maybe you'd like to add it? https://github.com/fatfingers23/esquema/issues/3 689 + 690 + let create_result = agent 691 + .api 692 + .com 693 + .atproto 694 + .repo 695 + .create_record( 696 + atrium_api::com::atproto::repo::create_record::InputData { 697 + collection: Status::NSID.parse().unwrap(), 698 + repo: did.into(), 699 + rkey: None, 700 + record: status.into(), 701 + swap_commit: None, 702 + validate: None, 703 + } 704 + .into(), 705 + ) 706 + .await; 707 + // ... 708 + } 709 + ``` 710 + > [!NOTE] 711 + > You will notice the first example used a string to serialize to Unknown, you could do something similar with 712 + > a struct you create, then serialize.But I created esquema to make that easier. 713 + > With esquema you can use other provided lexicons 714 + > or ones you create to build out the data structure for your ATProtocol application. 715 + > As well as in future updates it will honor the 716 + > validation you have in the Lexicon. 717 + > Things like string should be 10 long, etc. 718 + 719 + ## Step 6. Listening to the firehose 720 + 721 + > [!IMPORTANT] 722 + > It is important to note that the original tutorial they connect directly to the firehose, but in this one we use 723 + > [rocketman](https://crates.io/crates/rocketman) to connect to the Jetstream instead. 724 + > For most use cases this is fine and usually easier when using other clients than the Bluesky provided ones. 725 + > But it is important to note there are some differences that can 726 + > be found in their introduction to Jetstream article. 727 + > https://docs.bsky.app/blog/jetstream#tradeoffs-and-use-cases 728 + 729 + So far, we have: 730 + 731 + - Logged in via OAuth 732 + - Created a custom schema 733 + - Read & written records for the logged in user 734 + 735 + Now we want to fetch the status records from other users. 736 + 737 + Remember how we referred to our app as being like Google, crawling around the repos to get their records? One advantage 738 + we have in the AT Protocol is that each repo publishes an event log of their updates. 739 + 740 + ![A diagram of the event stream"](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-event-stream.aa119d83.png&w=1080&q=75) 741 + 742 + Using a [~~Relay~~ Jetstream service](https://docs.bsky.app/blog/jetstream) we can listen to an 743 + aggregated firehose of these events across all users in the network. In our case what we're looking for are valid 744 + `xyz.statusphere.status` records. 745 + 746 + ```rust 747 + /** ./src/ingester.rs **/ 748 + #[async_trait] 749 + impl LexiconIngestor for StatusSphereIngester { 750 + async fn ingest(&self, message: Event<Value>) -> anyhow::Result<()> { 751 + if let Some(commit) = &message.commit { 752 + //We manually construct the uri since jetstream does not provide it 753 + //at://{users did}/{collection: xyz.statusphere.status}{records key} 754 + let record_uri = format!("at://{}/{}/{}", message.did, commit.collection, commit.rkey); 755 + match commit.operation { 756 + Operation::Create | Operation::Update => { 757 + if let Some(record) = &commit.record { 758 + //We deserialize the record into our Rust struct 759 + let status_at_proto_record = serde_json::from_value::< 760 + lexicons::xyz::statusphere::status::RecordData, 761 + >(record.clone())?; 762 + 763 + if let Some(ref _cid) = commit.cid { 764 + // Although esquema does not have full validation yet, 765 + // if you get to this point, 766 + // You know the data structure is the same 767 + 768 + // Store the status 769 + // TODO 770 + } 771 + } 772 + } 773 + Operation::Delete => {}, 774 + } 775 + } else { 776 + return Err(anyhow!("Message has no commit")); 777 + } 778 + Ok(()) 779 + } 780 + } 781 + ``` 782 + 783 + Let's create a SQLite table to store these statuses: 784 + 785 + ```rust 786 + /** ./src/db.rs **/ 787 + // Create our statuses table 788 + pub async fn create_tables_in_database(pool: &Pool) -> Result<(), async_sqlite::Error> { 789 + pool.conn(move |conn| { 790 + conn.execute("PRAGMA foreign_keys = ON", []).unwrap(); 791 + 792 + // status 793 + conn.execute( 794 + "CREATE TABLE IF NOT EXISTS status ( 795 + uri TEXT PRIMARY KEY, 796 + authorDid TEXT NOT NULL, 797 + status TEXT NOT NULL, 798 + createdAt INTEGER NOT NULL, 799 + indexedAt INTEGER NOT NULL 800 + )", 801 + [], 802 + ) 803 + .unwrap(); 804 + 805 + // ... 806 + ``` 807 + 808 + Now we can write these statuses into our database as they arrive from the firehose: 809 + 810 + ```rust 811 + /** ./src/ingester.rs **/ 812 + // If the write is a valid status update 813 + if let Some(record) = &commit.record { 814 + let status_at_proto_record = serde_json::from_value::< 815 + lexicons::xyz::statusphere::status::RecordData, 816 + >(record.clone())?; 817 + 818 + if let Some(ref _cid) = commit.cid { 819 + // Although esquema does not have full validation yet, 820 + // if you get to this point, 821 + // You know the data structure is the same 822 + let created = status_at_proto_record.created_at.as_ref(); 823 + let right_now = chrono::Utc::now(); 824 + // We save or update the record in the db 825 + StatusFromDb { 826 + uri: record_uri, 827 + author_did: message.did.clone(), 828 + status: status_at_proto_record.status.clone(), 829 + created_at: created.to_utc(), 830 + indexed_at: right_now, 831 + handle: None, 832 + } 833 + .save_or_update(&self.db_pool) 834 + .await?; 835 + } 836 + } 837 + ``` 838 + 839 + You can almost think of information flowing in a loop: 840 + 841 + ![A diagram of the flow of information](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-info-flow.ccf81d0b.png&w=1080&q=75) 842 + 843 + Applications write to the repo. The write events are then emitted on the firehose where they're caught by the apps and 844 + ingested into their databases. 845 + 846 + Why sync from the event log like this? Because there are other apps in the network that will write the records we're 847 + interested in. By subscribing to the event log (via the Jetstream), we ensure that we catch all the data we're interested in &mdash; 848 + including data published by other apps! 849 + 850 + ## Step 7. Listing the latest statuses 851 + 852 + Now that we have statuses populating our SQLite, we can produce a timeline of status updates by users. We also use 853 + a [DID](https://atproto.com/specs/did)-to-handle resolver so we can show a nice username with the statuses: 854 + ```rust 855 + /** ./src/main.rs **/ 856 + // Homepage 857 + /// Home 858 + #[get("/")] 859 + async fn home( 860 + session: Session, 861 + oauth_client: web::Data<OAuthClientType>, 862 + db_pool: web::Data<Arc<Pool>>, 863 + handle_resolver: web::Data<HandleResolver>, 864 + ) -> Result<impl Responder> { 865 + const TITLE: &str = "Home"; 866 + // Fetch data stored in our SQLite 867 + let mut statuses = StatusFromDb::load_latest_statuses(&db_pool) 868 + .await 869 + .unwrap_or_else(|err| { 870 + log::error!("Error loading statuses: {err}"); 871 + vec![] 872 + }); 873 + 874 + // We resolve the handles to the DID. This is a bit messy atm, 875 + // and there are hopes to find a cleaner way 876 + // to handle resolving the DIDs and formating the handles, 877 + // But it gets the job done for the purpose of this tutorial. 878 + // PRs are welcomed! 879 + 880 + //Simple way to cut down on resolve calls if we already know the handle for the did 881 + let mut quick_resolve_map: HashMap<Did, String> = HashMap::new(); 882 + for db_status in &mut statuses { 883 + let authors_did = Did::new(db_status.author_did.clone()).expect("failed to parse did"); 884 + //Check to see if we already resolved it to cut down on resolve requests 885 + match quick_resolve_map.get(&authors_did) { 886 + None => {} 887 + Some(found_handle) => { 888 + db_status.handle = Some(found_handle.clone()); 889 + continue; 890 + } 891 + } 892 + //Attempts to resolve the DID to a handle 893 + db_status.handle = match handle_resolver.resolve(&authors_did).await { 894 + Ok(did_doc) => { 895 + match did_doc.also_known_as { 896 + None => None, 897 + Some(also_known_as) => { 898 + match also_known_as.is_empty() { 899 + true => None, 900 + false => { 901 + //also_known as a list starts the array with the highest priority handle 902 + let formatted_handle = 903 + format!("@{}", also_known_as[0]).replace("at://", ""); 904 + quick_resolve_map.insert(authors_did, formatted_handle.clone()); 905 + Some(formatted_handle) 906 + } 907 + } 908 + } 909 + } 910 + } 911 + Err(err) => { 912 + log::error!("Error resolving did: {err}"); 913 + None 914 + } 915 + }; 916 + } 917 + // ... 918 + ``` 919 + >[!NOTE] 920 + > We use a newly released handle resolver from atrium. 921 + > Can see 922 + > how it is set up in [./src/main.rs](https://github.com/fatfingers23/rusty_statusphere_example_app/blob/a13ab7eb8fcba901a483468f7fd7c56b2948972d/src/main.rs#L508) 923 + 924 + 925 + Our HTML can now list these status records: 926 + 927 + ```html 928 + <!-- ./templates/home.html --> 929 + {% for status in statuses %} 930 + <div class="{% if loop.first %} status-line no-line {% else %} status-line {% endif %} "> 931 + <div> 932 + <div class="status">{{status.status}}</div> 933 + </div> 934 + <div class="desc"> 935 + <a class="author" 936 + href="https://bsky.app/profile/{{status.author_did}}">{{status.author_display_name()}}</a> 937 + {% if status.is_today() %} 938 + is feeling {{status.status}} today 939 + {% else %} 940 + was feeling {{status.status}} on {{status.created_at}} 941 + {% endif %} 942 + </div> 943 + </div> 944 + {% endfor %} 945 + ` 946 + })} 947 + ``` 948 + 949 + ![A screenshot of the app status timeline](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp-status-history.25e5d14a.png&w=640&q=75) 950 + 951 + ## Step 8. Optimistic updates 952 + 953 + As a final optimization, let's introduce "optimistic updates." 954 + 955 + Remember the information flow loop with the repo write and the event log? 956 + 957 + !["A diagram of the flow of information"](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-info-flow.ccf81d0b.png&w=1080&q=75) 958 + 959 + Since we're updating our users' repos locally, we can short-circuit that flow to our own database: 960 + 961 + ![A diagram illustrating optimistic updates](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-optimistic-update.ca3f4cf1.png&w=1080&q=75) 962 + 963 + This is an important optimization to make, because it ensures that the user sees their own changes while using your app. 964 + When the event eventually arrives from the firehose, we just discard it since we already have it saved locally. 965 + 966 + To do this, we just update `POST /status` to include an additional write to our SQLite DB: 967 + 968 + ```rust 969 + /** ./src/main.rs **/ 970 + /// Creates a new status 971 + #[post("/status")] 972 + async fn status( 973 + request: HttpRequest, 974 + session: Session, 975 + oauth_client: web::Data<OAuthClientType>, 976 + db_pool: web::Data<Arc<Pool>>, 977 + form: web::Form<StatusForm>, 978 + ) -> HttpResponse { 979 + //... 980 + let create_result = agent 981 + .api 982 + .com 983 + .atproto 984 + .repo 985 + .create_record( 986 + atrium_api::com::atproto::repo::create_record::InputData { 987 + collection: Status::NSID.parse().unwrap(), 988 + repo: did.into(), 989 + rkey: None, 990 + record: status.into(), 991 + swap_commit: None, 992 + validate: None, 993 + } 994 + .into(), 995 + ) 996 + .await; 997 + 998 + match create_result { 999 + Ok(record) => { 1000 + let status = StatusFromDb::new( 1001 + record.uri.clone(), 1002 + did_string, 1003 + form.status.clone(), 1004 + ); 1005 + 1006 + let _ = status.save(db_pool).await; 1007 + Redirect::to("/") 1008 + .see_other() 1009 + .respond_to(&request) 1010 + .map_into_boxed_body() 1011 + } 1012 + Err(err) => { 1013 + log::error!("Error creating status: {err}"); 1014 + let error_html = ErrorTemplate { 1015 + title: "Error", 1016 + error: "Was an error creating the status, please check the logs.", 1017 + } 1018 + .render() 1019 + .expect("template should be valid"); 1020 + HttpResponse::Ok().body(error_html) 1021 + } 1022 + } 1023 + //... 1024 + } 1025 + ``` 1026 + 1027 + You'll notice this code looks almost exactly like what we're doing in `ingester.rs`. 1028 + 1029 + ## Thinking in AT Proto 1030 + 1031 + In this tutorial we've covered the key steps to building an atproto app. Data is published in its canonical form on 1032 + users' `at://` repos and then aggregated into apps' databases to produce views of the network. 1033 + 1034 + When building your app, think in these four key steps: 1035 + 1036 + - Design the [Lexicon](#) schemas for the records you'll publish into the Atmosphere. 1037 + - Create a database for aggregating the records into useful views. 1038 + - Build your application to write the records on your users' repos. 1039 + - Listen to the firehose to aggregate data across the network. 1040 + 1041 + Remember this flow of information throughout: 1042 + 1043 + ![A diagram of the flow of information](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-info-flow.ccf81d0b.png&w=1080&q=75) 1044 + 1045 + This is how every app in the Atmosphere works, including the [Bluesky social app](https://bsky.app). 1046 + 1047 + ## Next steps 1048 + 1049 + If you want to practice what you've learned, here are some additional challenges you could try: 1050 + 1051 + - Sync the profile records of all users so that you can show their display names instead of their handles. 1052 + - Count the number of each status used and display the total counts. 1053 + - Fetch the authed user's `app.bsky.graph.follow` follows and show statuses from them. 1054 + - Create a different kind of schema, like a way to post links to websites and rate them 1 through 4 stars. 1055 + 1056 + [Ready to learn more? Specs, guides, and SDKs can be found here.](https://atproto.com/) 1057 + 1058 + >[!NOTE] 1059 + > Thank you for checking out my version of the Statusphere example project! 1060 + > There are parts of this I feel can be improved on and made more efficient, 1061 + > but I think it does a good job for providing you with a starting point to start building Rust applications in the Atmosphere. 1062 + > See something you think could be done better? Then please submit a PR! 1063 + > [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev)
images/cover.png

This is a binary file and will not be displayed.

images/emojis.png

This is a binary file and will not be displayed.

+2
justfile
··· 1 + watch: 2 + watchexec -w src -w templates -r cargo run
+23
lexicons/status.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "xyz.statusphere.status", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "key": "tid", 8 + "record": { 9 + "type": "object", 10 + "required": ["status", "createdAt"], 11 + "properties": { 12 + "status": { 13 + "type": "string", 14 + "minLength": 1, 15 + "maxGraphemes": 1, 16 + "maxLength": 32 17 + }, 18 + "createdAt": { "type": "string", "format": "datetime" } 19 + } 20 + } 21 + } 22 + } 23 + }
-1
public/css/base.css
··· 1 - @import 'tailwindcss';
+3
rust-toolchain.toml
··· 1 + [toolchain] 2 + channel = "stable" 3 + version = "1.85.1"
-74
src/controllers/FeedController.rs
··· 1 - use super::BaseTemplate; 2 - use actix_web::{App, HttpServer, Responder, Result, Scope, get, middleware, web}; 3 - use askama::Template; 4 - use atrium_api::client::AtpServiceClient; 5 - use atrium_api::types::LimitedU32; 6 - use atrium_xrpc_client::reqwest::ReqwestClient; 7 - use std::{collections::HashMap, ops::Deref}; 8 - 9 - #[derive(Template)] 10 - #[template(path = "user.html")] 11 - struct UserTemplate<'a> { 12 - name: &'a str, 13 - text: &'a str, 14 - } 15 - 16 - #[derive(Template)] 17 - #[template(path = "feed.html")] 18 - struct FeedTemplate<'a> { 19 - _parent: &'a BaseTemplate<'a>, 20 - } 21 - 22 - impl<'a> Deref for FeedTemplate<'a> { 23 - type Target = BaseTemplate<'a>; 24 - 25 - fn deref(&self) -> &Self::Target { 26 - self._parent 27 - } 28 - } 29 - 30 - #[get("")] 31 - async fn index(query: web::Query<HashMap<String, String>>) -> Result<impl Responder> { 32 - let client = AtpServiceClient::new(ReqwestClient::new("https://public.api.bsky.app")); 33 - let feed = 34 - "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot".to_string(); 35 - 36 - let feed_posts = client 37 - .service 38 - .app 39 - .bsky 40 - .feed 41 - .get_feed( 42 - atrium_api::app::bsky::feed::get_feed::ParametersData { 43 - cursor: None, 44 - feed, 45 - limit: None, 46 - } 47 - .into(), 48 - ) 49 - .await; 50 - //Its working write out a nice thing to parse themS 51 - 52 - let html = if let Some(name) = query.get("name") { 53 - UserTemplate { 54 - name, 55 - text: "Welcome!", 56 - } 57 - .render() 58 - .expect("template should be valid") 59 - } else { 60 - FeedTemplate { 61 - _parent: &BaseTemplate { 62 - title: "Oh god not another bluesky client", 63 - }, 64 - } 65 - .render() 66 - .expect("template should be valid") 67 - }; 68 - 69 - Ok(web::Html::new(html)) 70 - } 71 - 72 - pub fn feed_controller() -> Scope { 73 - web::scope("/feed").service(index) 74 - }
-9
src/controllers/mod.rs
··· 1 - use askama::Template; 2 - 3 - pub mod FeedController; 4 - 5 - #[derive(Template)] 6 - #[template(path = "base.html")] 7 - struct BaseTemplate<'a> { 8 - title: &'a str, 9 - }
+408
src/db.rs
··· 1 + use actix_web::web::Data; 2 + use async_sqlite::{ 3 + Pool, rusqlite, 4 + rusqlite::{Error, Row}, 5 + }; 6 + use atrium_api::types::string::Did; 7 + use chrono::{DateTime, Datelike, Utc}; 8 + use rusqlite::types::Type; 9 + use serde::{Deserialize, Serialize}; 10 + use std::{fmt::Debug, sync::Arc}; 11 + 12 + /// Creates the tables in the db. 13 + pub async fn create_tables_in_database(pool: &Pool) -> Result<(), async_sqlite::Error> { 14 + pool.conn(move |conn| { 15 + conn.execute("PRAGMA foreign_keys = ON", []).unwrap(); 16 + 17 + // status 18 + conn.execute( 19 + "CREATE TABLE IF NOT EXISTS status ( 20 + uri TEXT PRIMARY KEY, 21 + authorDid TEXT NOT NULL, 22 + status TEXT NOT NULL, 23 + createdAt INTEGER NOT NULL, 24 + indexedAt INTEGER NOT NULL 25 + )", 26 + [], 27 + ) 28 + .unwrap(); 29 + 30 + // auth_session 31 + conn.execute( 32 + "CREATE TABLE IF NOT EXISTS auth_session ( 33 + key TEXT PRIMARY KEY, 34 + session TEXT NOT NULL 35 + )", 36 + [], 37 + ) 38 + .unwrap(); 39 + 40 + // auth_state 41 + conn.execute( 42 + "CREATE TABLE IF NOT EXISTS auth_state ( 43 + key TEXT PRIMARY KEY, 44 + state TEXT NOT NULL 45 + )", 46 + [], 47 + ) 48 + .unwrap(); 49 + Ok(()) 50 + }) 51 + .await?; 52 + Ok(()) 53 + } 54 + 55 + ///Status table datatype 56 + #[derive(Debug, Clone, Deserialize, Serialize)] 57 + pub struct StatusFromDb { 58 + pub uri: String, 59 + pub author_did: String, 60 + pub status: String, 61 + pub created_at: DateTime<Utc>, 62 + pub indexed_at: DateTime<Utc>, 63 + pub handle: Option<String>, 64 + } 65 + 66 + //Status methods 67 + impl StatusFromDb { 68 + /// Creates a new [StatusFromDb] 69 + pub fn new(uri: String, author_did: String, status: String) -> Self { 70 + let now = chrono::Utc::now(); 71 + Self { 72 + uri, 73 + author_did, 74 + status, 75 + created_at: now, 76 + indexed_at: now, 77 + handle: None, 78 + } 79 + } 80 + 81 + /// Helper to map from [Row] to [StatusDb] 82 + fn map_from_row(row: &Row) -> Result<Self, rusqlite::Error> { 83 + Ok(Self { 84 + uri: row.get(0)?, 85 + author_did: row.get(1)?, 86 + status: row.get(2)?, 87 + //DateTimes are stored as INTEGERS then parsed into a DateTime<UTC> 88 + created_at: { 89 + let timestamp: i64 = row.get(3)?; 90 + DateTime::from_timestamp(timestamp, 0).ok_or_else(|| { 91 + Error::InvalidColumnType(3, "Invalid timestamp".parse().unwrap(), Type::Text) 92 + })? 93 + }, 94 + //DateTimes are stored as INTEGERS then parsed into a DateTime<UTC> 95 + indexed_at: { 96 + let timestamp: i64 = row.get(4)?; 97 + DateTime::from_timestamp(timestamp, 0).ok_or_else(|| { 98 + Error::InvalidColumnType(4, "Invalid timestamp".parse().unwrap(), Type::Text) 99 + })? 100 + }, 101 + handle: None, 102 + }) 103 + } 104 + 105 + /// Helper for the UI to see if indexed_at date is today or not 106 + pub fn is_today(&self) -> bool { 107 + let now = Utc::now(); 108 + 109 + self.indexed_at.day() == now.day() 110 + && self.indexed_at.month() == now.month() 111 + && self.indexed_at.year() == now.year() 112 + } 113 + 114 + /// Saves the [StatusDb] 115 + pub async fn save(&self, pool: Data<Arc<Pool>>) -> Result<(), async_sqlite::Error> { 116 + let cloned_self = self.clone(); 117 + pool.conn(move |conn| { 118 + Ok(conn.execute( 119 + "INSERT INTO status (uri, authorDid, status, createdAt, indexedAt) VALUES (?1, ?2, ?3, ?4, ?5)", 120 + [ 121 + &cloned_self.uri, 122 + &cloned_self.author_did, 123 + &cloned_self.status, 124 + &cloned_self.created_at.timestamp().to_string(), 125 + &cloned_self.indexed_at.timestamp().to_string(), 126 + ], 127 + )?) 128 + }) 129 + .await?; 130 + Ok(()) 131 + } 132 + 133 + /// Saves or updates a status by its did(uri) 134 + pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> { 135 + let cloned_self = self.clone(); 136 + pool.conn(move |conn| { 137 + //We check to see if the session already exists, if so we need to update not insert 138 + let mut stmt = conn.prepare("SELECT COUNT(*) FROM status WHERE uri = ?1")?; 139 + let count: i64 = stmt.query_row([&cloned_self.uri], |row| row.get(0))?; 140 + match count > 0 { 141 + true => { 142 + let mut update_stmt = 143 + conn.prepare("UPDATE status SET status = ?2, indexedAt = ? WHERE uri = ?1")?; 144 + update_stmt.execute([&cloned_self.uri, &cloned_self.status, &cloned_self.indexed_at.timestamp().to_string()])?; 145 + Ok(()) 146 + } 147 + false => { 148 + conn.execute( 149 + "INSERT INTO status (uri, authorDid, status, createdAt, indexedAt) VALUES (?1, ?2, ?3, ?4, ?5)", 150 + [ 151 + &cloned_self.uri, 152 + &cloned_self.author_did, 153 + &cloned_self.status, 154 + &cloned_self.created_at.timestamp().to_string(), 155 + &cloned_self.indexed_at.timestamp().to_string(), 156 + ], 157 + )?; 158 + Ok(()) 159 + } 160 + } 161 + }) 162 + .await?; 163 + Ok(()) 164 + } 165 + pub async fn delete_by_uri(pool: &Pool, uri: String) -> Result<(), async_sqlite::Error> { 166 + pool.conn(move |conn| { 167 + let mut stmt = conn.prepare("DELETE FROM status WHERE uri = ?1")?; 168 + stmt.execute([&uri]) 169 + }) 170 + .await?; 171 + Ok(()) 172 + } 173 + 174 + /// Loads the last 10 statuses we have saved 175 + pub async fn load_latest_statuses( 176 + pool: &Data<Arc<Pool>>, 177 + ) -> Result<Vec<Self>, async_sqlite::Error> { 178 + Ok(pool 179 + .conn(move |conn| { 180 + let mut stmt = 181 + conn.prepare("SELECT * FROM status ORDER BY indexedAt DESC LIMIT 10")?; 182 + let status_iter = stmt 183 + .query_map([], |row| Ok(Self::map_from_row(row).unwrap())) 184 + .unwrap(); 185 + 186 + let mut statuses = Vec::new(); 187 + for status in status_iter { 188 + statuses.push(status?); 189 + } 190 + Ok(statuses) 191 + }) 192 + .await?) 193 + } 194 + 195 + /// Loads the logged-in users current status 196 + pub async fn my_status( 197 + pool: &Data<Arc<Pool>>, 198 + did: &str, 199 + ) -> Result<Option<Self>, async_sqlite::Error> { 200 + let did = did.to_string(); 201 + pool.conn(move |conn| { 202 + let mut stmt = conn.prepare( 203 + "SELECT * FROM status WHERE authorDid = ?1 ORDER BY createdAt DESC LIMIT 1", 204 + )?; 205 + stmt.query_row([did.as_str()], |row| Self::map_from_row(row)) 206 + .map(Some) 207 + .or_else(|err| { 208 + if err == rusqlite::Error::QueryReturnedNoRows { 209 + Ok(None) 210 + } else { 211 + Err(err) 212 + } 213 + }) 214 + }) 215 + .await 216 + } 217 + 218 + /// ui helper to show a handle or did if the handle cannot be found 219 + pub fn author_display_name(&self) -> String { 220 + match self.handle.as_ref() { 221 + Some(handle) => handle.to_string(), 222 + None => self.author_did.to_string(), 223 + } 224 + } 225 + } 226 + 227 + /// AuthSession table data type 228 + #[derive(Debug, Clone, Deserialize, Serialize)] 229 + pub struct AuthSession { 230 + pub key: String, 231 + pub session: String, 232 + } 233 + 234 + impl AuthSession { 235 + /// Creates a new [AuthSession] 236 + pub fn new<V>(key: String, session: V) -> Self 237 + where 238 + V: Serialize, 239 + { 240 + let session = serde_json::to_string(&session).unwrap(); 241 + Self { 242 + key: key.to_string(), 243 + session, 244 + } 245 + } 246 + 247 + /// Helper to map from [Row] to [AuthSession] 248 + fn map_from_row(row: &Row) -> Result<Self, Error> { 249 + let key: String = row.get(0)?; 250 + let session: String = row.get(1)?; 251 + Ok(Self { key, session }) 252 + } 253 + 254 + /// Gets a session by the users did(key) 255 + pub async fn get_by_did(pool: &Pool, did: String) -> Result<Option<Self>, async_sqlite::Error> { 256 + let did = Did::new(did).unwrap(); 257 + pool.conn(move |conn| { 258 + let mut stmt = conn.prepare("SELECT * FROM auth_session WHERE key = ?1")?; 259 + stmt.query_row([did.as_str()], |row| Self::map_from_row(row)) 260 + .map(Some) 261 + .or_else(|err| { 262 + if err == Error::QueryReturnedNoRows { 263 + Ok(None) 264 + } else { 265 + Err(err) 266 + } 267 + }) 268 + }) 269 + .await 270 + } 271 + 272 + /// Saves or updates the session by its did(key) 273 + pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> { 274 + let cloned_self = self.clone(); 275 + pool.conn(move |conn| { 276 + //We check to see if the session already exists, if so we need to update not insert 277 + let mut stmt = conn.prepare("SELECT COUNT(*) FROM auth_session WHERE key = ?1")?; 278 + let count: i64 = stmt.query_row([&cloned_self.key], |row| row.get(0))?; 279 + match count > 0 { 280 + true => { 281 + let mut update_stmt = 282 + conn.prepare("UPDATE auth_session SET session = ?2 WHERE key = ?1")?; 283 + update_stmt.execute([&cloned_self.key, &cloned_self.session])?; 284 + Ok(()) 285 + } 286 + false => { 287 + conn.execute( 288 + "INSERT INTO auth_session (key, session) VALUES (?1, ?2)", 289 + [&cloned_self.key, &cloned_self.session], 290 + )?; 291 + Ok(()) 292 + } 293 + } 294 + }) 295 + .await?; 296 + Ok(()) 297 + } 298 + 299 + /// Deletes the session by did 300 + pub async fn delete_by_did(pool: &Pool, did: String) -> Result<(), async_sqlite::Error> { 301 + pool.conn(move |conn| { 302 + let mut stmt = conn.prepare("DELETE FROM auth_session WHERE key = ?1")?; 303 + stmt.execute([&did]) 304 + }) 305 + .await?; 306 + Ok(()) 307 + } 308 + 309 + /// Deletes all the sessions 310 + pub async fn delete_all(pool: &Pool) -> Result<(), async_sqlite::Error> { 311 + pool.conn(move |conn| { 312 + let mut stmt = conn.prepare("DELETE FROM auth_session")?; 313 + stmt.execute([]) 314 + }) 315 + .await?; 316 + Ok(()) 317 + } 318 + } 319 + 320 + /// AuthState table datatype 321 + #[derive(Debug, Clone, Deserialize, Serialize)] 322 + pub struct AuthState { 323 + pub key: String, 324 + pub state: String, 325 + } 326 + 327 + impl AuthState { 328 + /// Creates a new [AuthState] 329 + pub fn new<V>(key: String, state: V) -> Self 330 + where 331 + V: Serialize, 332 + { 333 + let state = serde_json::to_string(&state).unwrap(); 334 + Self { 335 + key: key.to_string(), 336 + state, 337 + } 338 + } 339 + 340 + /// Helper to map from [Row] to [AuthState] 341 + fn map_from_row(row: &Row) -> Result<Self, Error> { 342 + let key: String = row.get(0)?; 343 + let state: String = row.get(1)?; 344 + Ok(Self { key, state }) 345 + } 346 + 347 + /// Gets a state by the users key 348 + pub async fn get_by_key(pool: &Pool, key: String) -> Result<Option<Self>, async_sqlite::Error> { 349 + pool.conn(move |conn| { 350 + let mut stmt = conn.prepare("SELECT * FROM auth_state WHERE key = ?1")?; 351 + stmt.query_row([key.as_str()], |row| Self::map_from_row(row)) 352 + .map(Some) 353 + .or_else(|err| { 354 + if err == Error::QueryReturnedNoRows { 355 + Ok(None) 356 + } else { 357 + Err(err) 358 + } 359 + }) 360 + }) 361 + .await 362 + } 363 + 364 + /// Saves or updates the state by its key 365 + pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> { 366 + let cloned_self = self.clone(); 367 + pool.conn(move |conn| { 368 + //We check to see if the state already exists, if so we need to update 369 + let mut stmt = conn.prepare("SELECT COUNT(*) FROM auth_state WHERE key = ?1")?; 370 + let count: i64 = stmt.query_row([&cloned_self.key], |row| row.get(0))?; 371 + match count > 0 { 372 + true => { 373 + let mut update_stmt = 374 + conn.prepare("UPDATE auth_state SET state = ?2 WHERE key = ?1")?; 375 + update_stmt.execute([&cloned_self.key, &cloned_self.state])?; 376 + Ok(()) 377 + } 378 + false => { 379 + conn.execute( 380 + "INSERT INTO auth_state (key, state) VALUES (?1, ?2)", 381 + [&cloned_self.key, &cloned_self.state], 382 + )?; 383 + Ok(()) 384 + } 385 + } 386 + }) 387 + .await?; 388 + Ok(()) 389 + } 390 + 391 + pub async fn delete_by_key(pool: &Pool, key: String) -> Result<(), async_sqlite::Error> { 392 + pool.conn(move |conn| { 393 + let mut stmt = conn.prepare("DELETE FROM auth_state WHERE key = ?1")?; 394 + stmt.execute([&key]) 395 + }) 396 + .await?; 397 + Ok(()) 398 + } 399 + 400 + pub async fn delete_all(pool: &Pool) -> Result<(), async_sqlite::Error> { 401 + pool.conn(move |conn| { 402 + let mut stmt = conn.prepare("DELETE FROM auth_state")?; 403 + stmt.execute([]) 404 + }) 405 + .await?; 406 + Ok(()) 407 + } 408 + }
+114
src/ingester.rs
··· 1 + use crate::db::StatusFromDb; 2 + use crate::lexicons; 3 + use crate::lexicons::xyz::statusphere::Status; 4 + use anyhow::anyhow; 5 + use async_sqlite::Pool; 6 + use async_trait::async_trait; 7 + use atrium_api::types::Collection; 8 + use log::error; 9 + use rocketman::{ 10 + connection::JetstreamConnection, 11 + handler, 12 + ingestion::LexiconIngestor, 13 + options::JetstreamOptions, 14 + types::event::{Event, Operation}, 15 + }; 16 + use serde_json::Value; 17 + use std::{ 18 + collections::HashMap, 19 + sync::{Arc, Mutex}, 20 + }; 21 + 22 + #[async_trait] 23 + impl LexiconIngestor for StatusSphereIngester { 24 + async fn ingest(&self, message: Event<Value>) -> anyhow::Result<()> { 25 + if let Some(commit) = &message.commit { 26 + //We manually construct the uri since Jetstream does not provide it 27 + //at://{users did}/{collection: xyz.statusphere.status}{records key} 28 + let record_uri = format!("at://{}/{}/{}", message.did, commit.collection, commit.rkey); 29 + match commit.operation { 30 + Operation::Create | Operation::Update => { 31 + if let Some(record) = &commit.record { 32 + let status_at_proto_record = serde_json::from_value::< 33 + lexicons::xyz::statusphere::status::RecordData, 34 + >(record.clone())?; 35 + 36 + if let Some(ref _cid) = commit.cid { 37 + // Although esquema does not have full validation yet, 38 + // if you get to this point, 39 + // You know the data structure is the same 40 + let created = status_at_proto_record.created_at.as_ref(); 41 + let right_now = chrono::Utc::now(); 42 + // We save or update the record in the db 43 + StatusFromDb { 44 + uri: record_uri, 45 + author_did: message.did.clone(), 46 + status: status_at_proto_record.status.clone(), 47 + created_at: created.to_utc(), 48 + indexed_at: right_now, 49 + handle: None, 50 + } 51 + .save_or_update(&self.db_pool) 52 + .await?; 53 + } 54 + } 55 + } 56 + Operation::Delete => StatusFromDb::delete_by_uri(&self.db_pool, record_uri).await?, 57 + } 58 + } else { 59 + return Err(anyhow!("Message has no commit")); 60 + } 61 + Ok(()) 62 + } 63 + } 64 + pub struct StatusSphereIngester { 65 + db_pool: Arc<Pool>, 66 + } 67 + 68 + pub async fn start_ingester(db_pool: Arc<Pool>) { 69 + // init the builder 70 + let opts = JetstreamOptions::builder() 71 + // your EXACT nsids 72 + // Which in this case is xyz.statusphere.status 73 + .wanted_collections(vec![Status::NSID.parse().unwrap()]) 74 + .build(); 75 + // create the jetstream connector 76 + let jetstream = JetstreamConnection::new(opts); 77 + 78 + // create your ingesters 79 + // Which in this case is xyz.statusphere.status 80 + let mut ingesters: HashMap<String, Box<dyn LexiconIngestor + Send + Sync>> = HashMap::new(); 81 + ingesters.insert( 82 + // your EXACT nsid 83 + Status::NSID.parse().unwrap(), 84 + Box::new(StatusSphereIngester { db_pool }), 85 + ); 86 + 87 + // tracks the last message we've processed 88 + let cursor: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None)); 89 + 90 + // get channels 91 + let msg_rx = jetstream.get_msg_rx(); 92 + let reconnect_tx = jetstream.get_reconnect_tx(); 93 + 94 + // spawn a task to process messages from the queue. 95 + // this is a simple implementation, you can use a more complex one based on needs. 96 + let c_cursor = cursor.clone(); 97 + tokio::spawn(async move { 98 + while let Ok(message) = msg_rx.recv_async().await { 99 + if let Err(e) = 100 + handler::handle_message(message, &ingesters, reconnect_tx.clone(), c_cursor.clone()) 101 + .await 102 + { 103 + error!("Error processing message: {}", e); 104 + }; 105 + } 106 + }); 107 + 108 + // connect to jetstream 109 + // retries internally, but may fail if there is an extreme error. 110 + if let Err(e) = jetstream.connect(cursor.clone()).await { 111 + error!("Failed to connect to Jetstream: {}", e); 112 + std::process::exit(1); 113 + } 114 + }
+3
src/lexicons/mod.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + pub mod record; 3 + pub mod xyz;
+23
src/lexicons/record.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!A collection of known record types. 3 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 4 + #[serde(tag = "$type")] 5 + pub enum KnownRecord { 6 + #[serde(rename = "xyz.statusphere.status")] 7 + LexiconsXyzStatusphereStatus(Box<crate::lexicons::xyz::statusphere::status::Record>), 8 + } 9 + impl From<crate::lexicons::xyz::statusphere::status::Record> for KnownRecord { 10 + fn from(record: crate::lexicons::xyz::statusphere::status::Record) -> Self { 11 + KnownRecord::LexiconsXyzStatusphereStatus(Box::new(record)) 12 + } 13 + } 14 + impl From<crate::lexicons::xyz::statusphere::status::RecordData> for KnownRecord { 15 + fn from(record_data: crate::lexicons::xyz::statusphere::status::RecordData) -> Self { 16 + KnownRecord::LexiconsXyzStatusphereStatus(Box::new(record_data.into())) 17 + } 18 + } 19 + impl Into<atrium_api::types::Unknown> for KnownRecord { 20 + fn into(self) -> atrium_api::types::Unknown { 21 + atrium_api::types::TryIntoUnknown::try_into_unknown(&self).unwrap() 22 + } 23 + }
+15
src/lexicons/xyz/statusphere/status.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `xyz.statusphere.status` namespace. 3 + use atrium_api::types::TryFromUnknown; 4 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 5 + #[serde(rename_all = "camelCase")] 6 + pub struct RecordData { 7 + pub created_at: atrium_api::types::string::Datetime, 8 + pub status: String, 9 + } 10 + pub type Record = atrium_api::types::Object<RecordData>; 11 + impl From<atrium_api::types::Unknown> for RecordData { 12 + fn from(value: atrium_api::types::Unknown) -> Self { 13 + Self::try_from_unknown(value).unwrap() 14 + } 15 + }
+9
src/lexicons/xyz/statusphere.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `xyz.statusphere` namespace. 3 + pub mod status; 4 + #[derive(Debug)] 5 + pub struct Status; 6 + impl atrium_api::types::Collection for Status { 7 + const NSID: &'static str = "xyz.statusphere.status"; 8 + type Record = status::Record; 9 + }
+3
src/lexicons/xyz.rs
··· 1 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 2 + //!Definitions for the `xyz` namespace. 3 + pub mod statusphere;
+562 -7
src/main.rs
··· 1 + use crate::{ 2 + db::{StatusFromDb, create_tables_in_database}, 3 + ingester::start_ingester, 4 + lexicons::record::KnownRecord, 5 + lexicons::xyz::statusphere::Status, 6 + storage::{SqliteSessionStore, SqliteStateStore}, 7 + templates::{HomeTemplate, LoginTemplate}, 8 + }; 1 9 use actix_files::Files; 2 - use actix_web::{App, HttpServer, Responder, Result, middleware, web}; 3 - use controllers::FeedController::feed_controller; 4 - use std::collections::HashMap; 10 + use actix_session::{ 11 + Session, SessionMiddleware, config::PersistentSession, storage::CookieSessionStore, 12 + }; 13 + use actix_web::{ 14 + App, HttpRequest, HttpResponse, HttpServer, Responder, Result, 15 + cookie::{self, Key}, 16 + get, middleware, post, 17 + web::{self, Redirect}, 18 + }; 19 + use askama::Template; 20 + use async_sqlite::{Pool, PoolBuilder}; 21 + use atrium_api::{ 22 + agent::Agent, 23 + types::Collection, 24 + types::string::{Datetime, Did}, 25 + }; 26 + use atrium_common::resolver::Resolver; 27 + use atrium_identity::{ 28 + did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL}, 29 + handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig}, 30 + }; 31 + use atrium_oauth::{ 32 + AtprotoLocalhostClientMetadata, AuthorizeOptions, CallbackParams, DefaultHttpClient, 33 + KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope, 34 + }; 35 + use dotenv::dotenv; 36 + use resolver::HickoryDnsTxtResolver; 37 + use serde::{Deserialize, Serialize}; 38 + use std::{ 39 + collections::HashMap, 40 + io::{Error, ErrorKind}, 41 + sync::Arc, 42 + }; 43 + use templates::{ErrorTemplate, Profile}; 44 + 45 + extern crate dotenv; 46 + 47 + mod db; 48 + mod ingester; 49 + mod lexicons; 50 + mod resolver; 51 + mod storage; 52 + mod templates; 53 + 54 + /// OAuthClientType to make it easier to access the OAuthClient in web requests 55 + type OAuthClientType = Arc< 56 + OAuthClient< 57 + SqliteStateStore, 58 + SqliteSessionStore, 59 + CommonDidResolver<DefaultHttpClient>, 60 + AtprotoHandleResolver<HickoryDnsTxtResolver, DefaultHttpClient>, 61 + >, 62 + >; 63 + 64 + /// HandleResolver to make it easier to access the OAuthClient in web requests 65 + type HandleResolver = Arc<CommonDidResolver<DefaultHttpClient>>; 66 + 67 + /// All the available emoji status options 68 + const STATUS_OPTIONS: [&str; 29] = [ 69 + "๐Ÿ‘", 70 + "๐Ÿ‘Ž", 71 + "๐Ÿ’™", 72 + "๐Ÿฅน", 73 + "๐Ÿ˜ง", 74 + "๐Ÿ˜ค", 75 + "๐Ÿ™ƒ", 76 + "๐Ÿ˜‰", 77 + "๐Ÿ˜Ž", 78 + "๐Ÿค“", 79 + "๐Ÿคจ", 80 + "๐Ÿฅณ", 81 + "๐Ÿ˜ญ", 82 + "๐Ÿ˜ค", 83 + "๐Ÿคฏ", 84 + "๐Ÿซก", 85 + "๐Ÿ’€", 86 + "โœŠ", 87 + "๐Ÿค˜", 88 + "๐Ÿ‘€", 89 + "๐Ÿง ", 90 + "๐Ÿ‘ฉโ€๐Ÿ’ป", 91 + "๐Ÿง‘โ€๐Ÿ’ป", 92 + "๐Ÿฅท", 93 + "๐ŸงŒ", 94 + "๐Ÿฆ‹", 95 + "๐Ÿš€", 96 + "๐Ÿฅ”", 97 + "๐Ÿฆ€", 98 + ]; 99 + 100 + /// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L71 101 + /// OAuth callback endpoint to complete session creation 102 + #[get("/oauth/callback")] 103 + async fn oauth_callback( 104 + request: HttpRequest, 105 + params: web::Query<CallbackParams>, 106 + oauth_client: web::Data<OAuthClientType>, 107 + session: Session, 108 + ) -> HttpResponse { 109 + //Processes the call back and parses out a session if found and valid 110 + match oauth_client.callback(params.into_inner()).await { 111 + Ok((bsky_session, _)) => { 112 + let agent = Agent::new(bsky_session); 113 + match agent.did().await { 114 + Some(did) => { 115 + session.insert("did", did).unwrap(); 116 + Redirect::to("/") 117 + .see_other() 118 + .respond_to(&request) 119 + .map_into_boxed_body() 120 + } 121 + None => { 122 + let html = ErrorTemplate { 123 + title: "Error", 124 + error: "The OAuth agent did not return a DID. May try re-logging in.", 125 + }; 126 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 127 + } 128 + } 129 + } 130 + Err(err) => { 131 + log::error!("Error: {err}"); 132 + let html = ErrorTemplate { 133 + title: "Error", 134 + error: "OAuth error, check the logs", 135 + }; 136 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 137 + } 138 + } 139 + } 140 + 141 + /// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L93 142 + /// Takes you to the login page 143 + #[get("/login")] 144 + async fn login() -> Result<impl Responder> { 145 + let html = LoginTemplate { 146 + title: "Log in", 147 + error: None, 148 + }; 149 + Ok(web::Html::new( 150 + html.render().expect("template should be valid"), 151 + )) 152 + } 153 + 154 + /// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L93 155 + /// Logs you out by destroying your cookie on the server and web browser 156 + #[get("/logout")] 157 + async fn logout(request: HttpRequest, session: Session) -> HttpResponse { 158 + session.purge(); 159 + Redirect::to("/") 160 + .see_other() 161 + .respond_to(&request) 162 + .map_into_boxed_body() 163 + } 164 + 165 + /// The post body for logging in 166 + #[derive(Serialize, Deserialize, Clone)] 167 + struct LoginForm { 168 + handle: String, 169 + } 170 + 171 + /// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L101 172 + /// Login endpoint 173 + #[post("/login")] 174 + async fn login_post( 175 + request: HttpRequest, 176 + params: web::Form<LoginForm>, 177 + oauth_client: web::Data<OAuthClientType>, 178 + ) -> HttpResponse { 179 + // This will act the same as the js method isValidHandle to make sure it is valid 180 + match atrium_api::types::string::Handle::new(params.handle.clone()) { 181 + Ok(handle) => { 182 + //Creates the oauth url to redirect to for the user to log in with their credentials 183 + let oauth_url = oauth_client 184 + .authorize( 185 + &handle, 186 + AuthorizeOptions { 187 + scopes: vec![ 188 + Scope::Known(KnownScope::Atproto), 189 + Scope::Known(KnownScope::TransitionGeneric), 190 + ], 191 + ..Default::default() 192 + }, 193 + ) 194 + .await; 195 + match oauth_url { 196 + Ok(url) => Redirect::to(url) 197 + .see_other() 198 + .respond_to(&request) 199 + .map_into_boxed_body(), 200 + Err(err) => { 201 + log::error!("Error: {err}"); 202 + let html = LoginTemplate { 203 + title: "Log in", 204 + error: Some("OAuth error"), 205 + }; 206 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 207 + } 208 + } 209 + } 210 + Err(err) => { 211 + let html: LoginTemplate<'_> = LoginTemplate { 212 + title: "Log in", 213 + error: Some(err), 214 + }; 215 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 216 + } 217 + } 218 + } 219 + 220 + /// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L146 221 + /// Home 222 + #[get("/")] 223 + async fn home( 224 + session: Session, 225 + oauth_client: web::Data<OAuthClientType>, 226 + db_pool: web::Data<Arc<Pool>>, 227 + handle_resolver: web::Data<HandleResolver>, 228 + ) -> Result<impl Responder> { 229 + const TITLE: &str = "Home"; 230 + //Loads the last 10 statuses saved in the DB 231 + let mut statuses = StatusFromDb::load_latest_statuses(&db_pool) 232 + .await 233 + .unwrap_or_else(|err| { 234 + log::error!("Error loading statuses: {err}"); 235 + vec![] 236 + }); 237 + 238 + //Simple way to cut down on resolve calls if we already know the handle for the did 239 + let mut quick_resolve_map: HashMap<Did, String> = HashMap::new(); 240 + // We resolve the handles to the DID. This is a bit messy atm, 241 + // and there are hopes to find a cleaner way 242 + // to handle resolving the DIDs and formating the handles, 243 + // But it gets the job done for the purpose of this tutorial. 244 + // PRs are welcomed! 245 + for db_status in &mut statuses { 246 + let authors_did = Did::new(db_status.author_did.clone()).expect("failed to parse did"); 247 + //Check to see if we already resolved it to cut down on resolve requests 248 + match quick_resolve_map.get(&authors_did) { 249 + None => {} 250 + Some(found_handle) => { 251 + db_status.handle = Some(found_handle.clone()); 252 + continue; 253 + } 254 + } 255 + //Attempts to resolve the DID to a handle 256 + db_status.handle = match handle_resolver.resolve(&authors_did).await { 257 + Ok(did_doc) => { 258 + match did_doc.also_known_as { 259 + None => None, 260 + Some(also_known_as) => { 261 + match also_known_as.is_empty() { 262 + true => None, 263 + false => { 264 + //also_known as a list starts the array with the highest priority handle 265 + let formatted_handle = 266 + format!("@{}", also_known_as[0]).replace("at://", ""); 267 + quick_resolve_map.insert(authors_did, formatted_handle.clone()); 268 + Some(formatted_handle) 269 + } 270 + } 271 + } 272 + } 273 + } 274 + Err(err) => { 275 + log::error!("Error resolving did: {err}"); 276 + None 277 + } 278 + }; 279 + } 280 + 281 + // If the user is signed in, get an agent which communicates with their server 282 + match session.get::<String>("did").unwrap_or(None) { 283 + Some(did) => { 284 + let did = Did::new(did).expect("failed to parse did"); 285 + //Grabs the users last status to highlight it in the ui 286 + let my_status = StatusFromDb::my_status(&db_pool, &did) 287 + .await 288 + .unwrap_or_else(|err| { 289 + log::error!("Error loading my status: {err}"); 290 + None 291 + }); 292 + 293 + // gets the user's session from the session store to resume 294 + match oauth_client.restore(&did).await { 295 + Ok(session) => { 296 + //Creates an agent to make authenticated requests 297 + let agent = Agent::new(session); 298 + 299 + // Fetch additional information about the logged-in user 300 + let profile = agent 301 + .api 302 + .app 303 + .bsky 304 + .actor 305 + .get_profile( 306 + atrium_api::app::bsky::actor::get_profile::ParametersData { 307 + actor: atrium_api::types::string::AtIdentifier::Did(did), 308 + } 309 + .into(), 310 + ) 311 + .await; 312 + 313 + let html = HomeTemplate { 314 + title: TITLE, 315 + status_options: &STATUS_OPTIONS, 316 + profile: match profile { 317 + Ok(profile) => { 318 + let profile_data = Profile { 319 + did: profile.did.to_string(), 320 + display_name: profile.display_name.clone(), 321 + }; 322 + Some(profile_data) 323 + } 324 + Err(err) => { 325 + log::error!("Error accessing profile: {err}"); 326 + None 327 + } 328 + }, 329 + statuses, 330 + my_status: my_status.as_ref().map(|s| s.status.clone()), 331 + } 332 + .render() 333 + .expect("template should be valid"); 5 334 6 - pub mod controllers; 335 + Ok(web::Html::new(html)) 336 + } 337 + Err(err) => { 338 + // Destroys the system or you're in a loop 339 + session.purge(); 340 + log::error!("Error restoring session: {err}"); 341 + let error_html = ErrorTemplate { 342 + title: "Error", 343 + error: "Was an error resuming the session, please check the logs.", 344 + } 345 + .render() 346 + .expect("template should be valid"); 347 + Ok(web::Html::new(error_html)) 348 + } 349 + } 350 + } 351 + 352 + None => { 353 + let html = HomeTemplate { 354 + title: TITLE, 355 + status_options: &STATUS_OPTIONS, 356 + profile: None, 357 + statuses, 358 + my_status: None, 359 + } 360 + .render() 361 + .expect("template should be valid"); 362 + 363 + Ok(web::Html::new(html)) 364 + } 365 + } 366 + } 367 + 368 + /// The post body for changing your status 369 + #[derive(Serialize, Deserialize, Clone)] 370 + struct StatusForm { 371 + status: String, 372 + } 373 + 374 + /// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L208 375 + /// Creates a new status 376 + #[post("/status")] 377 + async fn status( 378 + request: HttpRequest, 379 + session: Session, 380 + oauth_client: web::Data<OAuthClientType>, 381 + db_pool: web::Data<Arc<Pool>>, 382 + form: web::Form<StatusForm>, 383 + ) -> HttpResponse { 384 + // Check if the user is logged in 385 + match session.get::<String>("did").unwrap_or(None) { 386 + Some(did_string) => { 387 + let did = Did::new(did_string.clone()).expect("failed to parse did"); 388 + // gets the user's session from the session store to resume 389 + match oauth_client.restore(&did).await { 390 + Ok(session) => { 391 + let agent = Agent::new(session); 392 + //Creates a strongly typed ATProto record 393 + let status: KnownRecord = lexicons::xyz::statusphere::status::RecordData { 394 + created_at: Datetime::now(), 395 + status: form.status.clone(), 396 + } 397 + .into(); 398 + 399 + // TODO no data validation yet from esquema 400 + // Maybe you'd like to add it? https://github.com/fatfingers23/esquema/issues/3 401 + 402 + let create_result = agent 403 + .api 404 + .com 405 + .atproto 406 + .repo 407 + .create_record( 408 + atrium_api::com::atproto::repo::create_record::InputData { 409 + collection: Status::NSID.parse().unwrap(), 410 + repo: did.into(), 411 + rkey: None, 412 + record: status.into(), 413 + swap_commit: None, 414 + validate: None, 415 + } 416 + .into(), 417 + ) 418 + .await; 419 + 420 + match create_result { 421 + Ok(record) => { 422 + let status = StatusFromDb::new( 423 + record.uri.clone(), 424 + did_string, 425 + form.status.clone(), 426 + ); 427 + 428 + let _ = status.save(db_pool).await; 429 + Redirect::to("/") 430 + .see_other() 431 + .respond_to(&request) 432 + .map_into_boxed_body() 433 + } 434 + Err(err) => { 435 + log::error!("Error creating status: {err}"); 436 + let error_html = ErrorTemplate { 437 + title: "Error", 438 + error: "Was an error creating the status, please check the logs.", 439 + } 440 + .render() 441 + .expect("template should be valid"); 442 + HttpResponse::Ok().body(error_html) 443 + } 444 + } 445 + } 446 + Err(err) => { 447 + // Destroys the system or you're in a loop 448 + session.purge(); 449 + log::error!( 450 + "Error restoring session, we are removing the session from the cookie: {err}" 451 + ); 452 + let error_html = ErrorTemplate { 453 + title: "Error", 454 + error: "Was an error resuming the session, please check the logs.", 455 + } 456 + .render() 457 + .expect("template should be valid"); 458 + HttpResponse::Ok().body(error_html) 459 + } 460 + } 461 + } 462 + None => { 463 + let error_template = ErrorTemplate { 464 + title: "Error", 465 + error: "You must be logged in to create a status.", 466 + } 467 + .render() 468 + .expect("template should be valid"); 469 + HttpResponse::Ok().body(error_template) 470 + } 471 + } 472 + } 7 473 8 474 #[actix_web::main] 9 475 async fn main() -> std::io::Result<()> { 476 + dotenv().ok(); 10 477 env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 478 + let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); 479 + let port = std::env::var("PORT") 480 + .unwrap_or_else(|_| "8080".to_string()) 481 + .parse::<u16>() 482 + .unwrap_or(8080); 11 483 12 - log::info!("starting HTTP server at http://localhost:8080"); 484 + //Uses a default sqlite db path or use the one from env 485 + let db_connection_string = 486 + std::env::var("DB_PATH").unwrap_or_else(|_| String::from("./statusphere.sqlite3")); 487 + 488 + //Crates a db pool to share resources to the db 489 + let pool = match PoolBuilder::new().path(db_connection_string).open().await { 490 + Ok(pool) => pool, 491 + Err(err) => { 492 + log::error!("Error creating the sqlite pool: {}", err); 493 + return Err(Error::new( 494 + ErrorKind::Other, 495 + "sqlite pool could not be created.", 496 + )); 497 + } 498 + }; 13 499 500 + //Creates the DB and tables 501 + create_tables_in_database(&pool) 502 + .await 503 + .expect("Could not create the database"); 504 + 505 + //Create a new handle resolver for the home page 506 + let http_client = Arc::new(DefaultHttpClient::default()); 507 + 508 + let handle_resolver = CommonDidResolver::new(CommonDidResolverConfig { 509 + plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 510 + http_client: http_client.clone(), 511 + }); 512 + let handle_resolver = Arc::new(handle_resolver); 513 + 514 + // Create a new OAuth client 515 + let http_client = Arc::new(DefaultHttpClient::default()); 516 + let config = OAuthClientConfig { 517 + client_metadata: AtprotoLocalhostClientMetadata { 518 + redirect_uris: Some(vec![String::from(format!( 519 + //This must match the endpoint you use the callback function 520 + "http://{host}:{port}/oauth/callback" 521 + ))]), 522 + scopes: Some(vec![ 523 + Scope::Known(KnownScope::Atproto), 524 + Scope::Known(KnownScope::TransitionGeneric), 525 + ]), 526 + }, 527 + keys: None, 528 + resolver: OAuthResolverConfig { 529 + did_resolver: CommonDidResolver::new(CommonDidResolverConfig { 530 + plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(), 531 + http_client: http_client.clone(), 532 + }), 533 + handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig { 534 + dns_txt_resolver: HickoryDnsTxtResolver::default(), 535 + http_client: http_client.clone(), 536 + }), 537 + authorization_server_metadata: Default::default(), 538 + protected_resource_metadata: Default::default(), 539 + }, 540 + state_store: SqliteStateStore::new(pool.clone()), 541 + session_store: SqliteSessionStore::new(pool.clone()), 542 + }; 543 + let client = Arc::new(OAuthClient::new(config).expect("failed to create OAuth client")); 544 + let arc_pool = Arc::new(pool.clone()); 545 + //Spawns the ingester that listens for other's Statusphere updates 546 + tokio::spawn(async move { 547 + start_ingester(arc_pool).await; 548 + }); 549 + let arc_pool = Arc::new(pool.clone()); 550 + log::info!("starting HTTP server at http://{host}:{port}"); 14 551 HttpServer::new(move || { 15 552 App::new() 16 553 .wrap(middleware::Logger::default()) 554 + .app_data(web::Data::new(client.clone())) 555 + .app_data(web::Data::new(arc_pool.clone())) 556 + .app_data(web::Data::new(handle_resolver.clone())) 557 + .wrap( 558 + SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64])) 559 + //TODO will need to set to true in production 560 + .cookie_secure(false) 561 + // customize session and cookie expiration 562 + .session_lifecycle( 563 + PersistentSession::default().session_ttl(cookie::time::Duration::days(14)), 564 + ) 565 + .build(), 566 + ) 17 567 .service(Files::new("/css", "public/css").show_files_listing()) 18 - .service(feed_controller()) 568 + .service(oauth_callback) 569 + .service(login) 570 + .service(login_post) 571 + .service(logout) 572 + .service(home) 573 + .service(status) 19 574 }) 20 - .bind(("127.0.0.1", 8080))? 575 + .bind(("127.0.0.1", port))? 21 576 .run() 22 577 .await 23 578 }
+32
src/resolver.rs
··· 1 + use atrium_identity::handle::DnsTxtResolver; 2 + use hickory_resolver::TokioAsyncResolver; 3 + 4 + /// Setup for dns resolver for the handle resolver 5 + pub struct HickoryDnsTxtResolver { 6 + resolver: hickory_resolver::TokioAsyncResolver, 7 + } 8 + 9 + impl Default for HickoryDnsTxtResolver { 10 + fn default() -> Self { 11 + Self { 12 + resolver: TokioAsyncResolver::tokio_from_system_conf() 13 + .expect("failed to create resolver"), 14 + } 15 + } 16 + } 17 + 18 + impl DnsTxtResolver for HickoryDnsTxtResolver { 19 + async fn resolve( 20 + &self, 21 + query: &str, 22 + ) -> core::result::Result<Vec<String>, Box<dyn std::error::Error + Send + Sync + 'static>> { 23 + println!("Resolving TXT for: {}", query); 24 + Ok(self 25 + .resolver 26 + .txt_lookup(query) 27 + .await? 28 + .iter() 29 + .map(|txt| txt.to_string()) 30 + .collect()) 31 + } 32 + }
+145
src/storage.rs
··· 1 + /// Storage impls to persis OAuth sessions if you are not using the memory stores 2 + /// https://github.com/bluesky-social/statusphere-example-app/blob/main/src/auth/storage.ts 3 + use crate::db::{AuthSession, AuthState}; 4 + use async_sqlite::Pool; 5 + use atrium_api::types::string::Did; 6 + use atrium_common::store::Store; 7 + use atrium_oauth::store::session::SessionStore; 8 + use atrium_oauth::store::state::StateStore; 9 + use serde::Serialize; 10 + use serde::de::DeserializeOwned; 11 + use std::fmt::Debug; 12 + use std::hash::Hash; 13 + use thiserror::Error; 14 + 15 + #[derive(Error, Debug)] 16 + pub enum SqliteStoreError { 17 + #[error("Invalid session")] 18 + InvalidSession, 19 + #[error("No session found")] 20 + NoSessionFound, 21 + #[error("Database error: {0}")] 22 + DatabaseError(async_sqlite::Error), 23 + } 24 + 25 + ///Persistent session store in sqlite 26 + impl SessionStore for SqliteSessionStore {} 27 + 28 + pub struct SqliteSessionStore { 29 + db_pool: Pool, 30 + } 31 + 32 + impl SqliteSessionStore { 33 + pub fn new(db: Pool) -> Self { 34 + Self { db_pool: db } 35 + } 36 + } 37 + 38 + impl<K, V> Store<K, V> for SqliteSessionStore 39 + where 40 + K: Debug + Eq + Hash + Send + Sync + 'static + From<Did> + AsRef<str>, 41 + V: Debug + Clone + Send + Sync + 'static + Serialize + DeserializeOwned, 42 + { 43 + type Error = SqliteStoreError; 44 + async fn get(&self, key: &K) -> Result<Option<V>, Self::Error> { 45 + let did = key.as_ref().to_string(); 46 + match AuthSession::get_by_did(&self.db_pool, did).await { 47 + Ok(Some(auth_session)) => { 48 + let deserialized_session: V = serde_json::from_str(&auth_session.session) 49 + .map_err(|_| SqliteStoreError::InvalidSession)?; 50 + Ok(Some(deserialized_session)) 51 + } 52 + Ok(None) => Err(SqliteStoreError::NoSessionFound), 53 + Err(db_error) => { 54 + log::error!("Database error: {db_error}"); 55 + Err(SqliteStoreError::DatabaseError(db_error)) 56 + } 57 + } 58 + } 59 + 60 + async fn set(&self, key: K, value: V) -> Result<(), Self::Error> { 61 + let did = key.as_ref().to_string(); 62 + let auth_session = AuthSession::new(did, value); 63 + auth_session 64 + .save_or_update(&self.db_pool) 65 + .await 66 + .map_err(SqliteStoreError::DatabaseError)?; 67 + Ok(()) 68 + } 69 + 70 + async fn del(&self, _key: &K) -> Result<(), Self::Error> { 71 + let did = _key.as_ref().to_string(); 72 + AuthSession::delete_by_did(&self.db_pool, did) 73 + .await 74 + .map_err(SqliteStoreError::DatabaseError)?; 75 + Ok(()) 76 + } 77 + 78 + async fn clear(&self) -> Result<(), Self::Error> { 79 + AuthSession::delete_all(&self.db_pool) 80 + .await 81 + .map_err(SqliteStoreError::DatabaseError)?; 82 + Ok(()) 83 + } 84 + } 85 + 86 + ///Persistent session state in sqlite 87 + impl StateStore for SqliteStateStore {} 88 + 89 + pub struct SqliteStateStore { 90 + db_pool: Pool, 91 + } 92 + 93 + impl SqliteStateStore { 94 + pub fn new(db: Pool) -> Self { 95 + Self { db_pool: db } 96 + } 97 + } 98 + 99 + impl<K, V> Store<K, V> for SqliteStateStore 100 + where 101 + K: Debug + Eq + Hash + Send + Sync + 'static + From<Did> + AsRef<str>, 102 + V: Debug + Clone + Send + Sync + 'static + Serialize + DeserializeOwned, 103 + { 104 + type Error = SqliteStoreError; 105 + async fn get(&self, key: &K) -> Result<Option<V>, Self::Error> { 106 + let key = key.as_ref().to_string(); 107 + match AuthState::get_by_key(&self.db_pool, key).await { 108 + Ok(Some(auth_state)) => { 109 + let deserialized_state: V = serde_json::from_str(&auth_state.state) 110 + .map_err(|_| SqliteStoreError::InvalidSession)?; 111 + Ok(Some(deserialized_state)) 112 + } 113 + Ok(None) => Err(SqliteStoreError::NoSessionFound), 114 + Err(db_error) => { 115 + log::error!("Database error: {db_error}"); 116 + Err(SqliteStoreError::DatabaseError(db_error)) 117 + } 118 + } 119 + } 120 + 121 + async fn set(&self, key: K, value: V) -> Result<(), Self::Error> { 122 + let did = key.as_ref().to_string(); 123 + let auth_state = AuthState::new(did, value); 124 + auth_state 125 + .save_or_update(&self.db_pool) 126 + .await 127 + .map_err(SqliteStoreError::DatabaseError)?; 128 + Ok(()) 129 + } 130 + 131 + async fn del(&self, _key: &K) -> Result<(), Self::Error> { 132 + let key = _key.as_ref().to_string(); 133 + AuthState::delete_by_key(&self.db_pool, key) 134 + .await 135 + .map_err(SqliteStoreError::DatabaseError)?; 136 + Ok(()) 137 + } 138 + 139 + async fn clear(&self) -> Result<(), Self::Error> { 140 + AuthState::delete_all(&self.db_pool) 141 + .await 142 + .map_err(SqliteStoreError::DatabaseError)?; 143 + Ok(()) 144 + } 145 + }
+35
src/templates.rs
··· 1 + ///The askama template types for HTML 2 + /// 3 + use crate::db::StatusFromDb; 4 + use askama::Template; 5 + use serde::{Deserialize, Serialize}; 6 + 7 + #[derive(Template)] 8 + #[template(path = "home.html")] 9 + pub struct HomeTemplate<'a> { 10 + pub title: &'a str, 11 + pub status_options: &'a [&'a str], 12 + pub profile: Option<Profile>, 13 + pub statuses: Vec<StatusFromDb>, 14 + pub my_status: Option<String>, 15 + } 16 + 17 + #[derive(Serialize, Deserialize, Debug, Clone)] 18 + pub struct Profile { 19 + pub did: String, 20 + pub display_name: Option<String>, 21 + } 22 + 23 + #[derive(Template)] 24 + #[template(path = "login.html")] 25 + pub struct LoginTemplate<'a> { 26 + pub title: &'a str, 27 + pub error: Option<&'a str>, 28 + } 29 + 30 + #[derive(Template)] 31 + #[template(path = "error.html")] 32 + pub struct ErrorTemplate<'a> { 33 + pub title: &'a str, 34 + pub error: &'a str, 35 + }
-1
templates/base.html
··· 10 10 11 11 <body> 12 12 13 - 14 13 {% block content %}{% endblock %} 15 14 16 15 </body>
+10
templates/error.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div id="root"> 5 + <p>Error: <i>{{error}}</i></p> 6 + <br> 7 + <a href="/">Go Home</a> 8 + </div> 9 + 10 + {%endblock content%}
-18
templates/feed.html
··· 1 - {% extends "base.html" %} 2 - 3 - {% block content %} 4 - 5 - <div> 6 - <h1 class="text-3xl font-bold underline text-clifford"> 7 - Hello world! 8 - </h1> 9 - <h1>Welcome!</h1> 10 - <p> 11 - <h3>What is your name?</h3> 12 - <form> 13 - <input class="input" type="text" name="name" /><br /> 14 - <p><input type="submit"></p> 15 - </form> 16 - </p> 17 - </div> 18 - {%endblock content%}
+67
templates/home.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div id="root"> 5 + <div class="error"></div> 6 + <div id="header"> 7 + <h1>Rusty Statusphere</h1> 8 + <p>Set your status on the Atmosphere.</p> 9 + </div> 10 + <div class="container"> 11 + <div class="card"> 12 + {% if let Some(Profile {did, display_name}) = profile %} 13 + <form action="/logout" method="get" class="session-form"> 14 + <div> 15 + Hi, 16 + {% if let Some(display_name) = display_name %} 17 + <strong>{{display_name}}</strong> 18 + {% else %} 19 + <strong>friend</strong> 20 + {% endif %}. What's 21 + your status today?? 22 + </div> 23 + <div> 24 + <button type="submit">Log out</button> 25 + </div> 26 + </form> 27 + {% else %} 28 + <div class="session-form"> 29 + <div><a href="/login">Log in</a> to set your status!</div> 30 + <div> 31 + <a href="/login" class="button">Log in</a> 32 + </div> 33 + </div> 34 + {% endif %} 35 + 36 + 37 + </div> 38 + <form action="/status" method="post" class="status-options"> 39 + {% for status in status_options %} 40 + <button 41 + class="{% if let Some(my_status) = my_status %} {%if my_status == status %} status-option selected {% else %} status-option {% endif %} {% else %} status-option {%endif%} " 42 + name="status" value="{{status}}"> 43 + {{status}} 44 + </button> 45 + 46 + {% endfor %} 47 + </form> 48 + {% for status in statuses %} 49 + <div class="{% if loop.first %} status-line no-line {% else %} status-line {% endif %} "> 50 + <div> 51 + <div class="status">{{status.status}}</div> 52 + </div> 53 + <div class="desc"> 54 + <a class="author" 55 + href="https://bsky.app/profile/{{status.author_did}}">{{status.author_display_name()}}</a> 56 + {% if status.is_today() %} 57 + is feeling {{status.status}} today 58 + {% else %} 59 + was feeling {{status.status}} on {{status.created_at}} 60 + {% endif %} 61 + </div> 62 + </div> 63 + {% endfor %} 64 + </div> 65 + </div> 66 + 67 + {%endblock content%}
+24
templates/login.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block content %} 4 + <div id="root"> 5 + <div id="header"> 6 + <h1>Rusty Statusphere</h1> 7 + <p>Set your status on the Atmosphere.</p> 8 + </div> 9 + <div class="container"> 10 + <form action="/login" method="post" class="login-form"> 11 + <input type="text" name="handle" placeholder="Enter your handle (eg alice.bsky.social)" required/> 12 + <button type="submit">Log in</button> 13 + {% if let Some(error) = self.error %} 14 + <p>Error: <i>{{error}}</i></p> 15 + {% endif %} 16 + </form> 17 + <div class="signup-cta"> 18 + Don't have an account on the Atmosphere? 19 + <a href="https://bsky.app">Sign up for Bluesky</a> to create one now! 20 + </div> 21 + </div> 22 + </div> 23 + 24 + {%endblock content%}
-17
templates/user.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - 4 - <head> 5 - <meta charset="utf-8" /> 6 - <title>Actix Web</title> 7 - </head> 8 - 9 - <body> 10 - <h1>Hi, {{ name }}!</h1> 11 - <p> 12 - {{ text }} 13 - {{ name }} 14 - </p> 15 - </body> 16 - 17 - </html>