Learn how to use Rust to build ATProto powered applications

V 1.0

+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
+1060 -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" 2055 + source = "registry+https://github.com/rust-lang/crates.io-index" 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" 1626 2066 source = "registry+https://github.com/rust-lang/crates.io-index" 1627 - checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" 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.1.1" 2720 + source = "registry+https://github.com/rust-lang/crates.io-index" 2721 + checksum = "72eca7751d4341e1ec6227b2300aed5c86c48b58cdae4a6e41e5c3ad7522ec2c" 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 + ] 2739 + 2740 + [[package]] 2741 + name = "rusqlite" 2742 + version = "0.33.0" 2743 + source = "registry+https://github.com/rust-lang/crates.io-index" 2744 + checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110" 2745 + dependencies = [ 2746 + "bitflags", 2747 + "fallible-iterator", 2748 + "fallible-streaming-iterator", 2749 + "hashlink", 2750 + "libsqlite3-sys", 2751 + "smallvec", 2752 + ] 2753 + 2754 + [[package]] 2163 2755 name = "rustc-demangle" 2164 2756 version = "0.1.24" 2165 2757 source = "registry+https://github.com/rust-lang/crates.io-index" 2166 2758 checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 2167 2759 2168 2760 [[package]] 2761 + name = "rustc-hash" 2762 + version = "2.1.1" 2763 + source = "registry+https://github.com/rust-lang/crates.io-index" 2764 + checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 2765 + 2766 + [[package]] 2169 2767 name = "rustc_version" 2170 2768 version = "0.4.1" 2171 2769 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2188 2786 ] 2189 2787 2190 2788 [[package]] 2789 + name = "rustls" 2790 + version = "0.21.12" 2791 + source = "registry+https://github.com/rust-lang/crates.io-index" 2792 + checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" 2793 + dependencies = [ 2794 + "log", 2795 + "ring", 2796 + "rustls-webpki", 2797 + "sct", 2798 + ] 2799 + 2800 + [[package]] 2801 + name = "rustls-native-certs" 2802 + version = "0.6.3" 2803 + source = "registry+https://github.com/rust-lang/crates.io-index" 2804 + checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" 2805 + dependencies = [ 2806 + "openssl-probe", 2807 + "rustls-pemfile 1.0.4", 2808 + "schannel", 2809 + "security-framework", 2810 + ] 2811 + 2812 + [[package]] 2813 + name = "rustls-pemfile" 2814 + version = "1.0.4" 2815 + source = "registry+https://github.com/rust-lang/crates.io-index" 2816 + checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" 2817 + dependencies = [ 2818 + "base64 0.21.7", 2819 + ] 2820 + 2821 + [[package]] 2191 2822 name = "rustls-pemfile" 2192 2823 version = "2.2.0" 2193 2824 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2203 2834 checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 2204 2835 2205 2836 [[package]] 2837 + name = "rustls-webpki" 2838 + version = "0.101.7" 2839 + source = "registry+https://github.com/rust-lang/crates.io-index" 2840 + checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" 2841 + dependencies = [ 2842 + "ring", 2843 + "untrusted", 2844 + ] 2845 + 2846 + [[package]] 2206 2847 name = "rustversion" 2207 2848 version = "1.0.20" 2208 2849 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2217 2858 "actix-web", 2218 2859 "anyhow", 2219 2860 "askama", 2220 - "async-stream", 2861 + "async-sqlite", 2862 + "async-trait", 2221 2863 "atrium-api", 2222 - "atrium-xrpc-client", 2864 + "atrium-common", 2865 + "atrium-identity", 2866 + "atrium-oauth", 2867 + "chrono", 2868 + "dotenv", 2223 2869 "env_logger", 2870 + "hickory-resolver", 2224 2871 "log", 2872 + "rocketman", 2225 2873 "serde", 2226 2874 "serde_json", 2875 + "thiserror", 2876 + "tokio", 2227 2877 ] 2228 2878 2229 2879 [[package]] ··· 2254 2904 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 2255 2905 2256 2906 [[package]] 2907 + name = "sct" 2908 + version = "0.7.1" 2909 + source = "registry+https://github.com/rust-lang/crates.io-index" 2910 + checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" 2911 + dependencies = [ 2912 + "ring", 2913 + "untrusted", 2914 + ] 2915 + 2916 + [[package]] 2917 + name = "sec1" 2918 + version = "0.7.3" 2919 + source = "registry+https://github.com/rust-lang/crates.io-index" 2920 + checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" 2921 + dependencies = [ 2922 + "base16ct", 2923 + "der", 2924 + "generic-array", 2925 + "subtle", 2926 + "zeroize", 2927 + ] 2928 + 2929 + [[package]] 2257 2930 name = "security-framework" 2258 2931 version = "2.11.1" 2259 2932 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2284 2957 2285 2958 [[package]] 2286 2959 name = "serde" 2287 - version = "1.0.218" 2960 + version = "1.0.219" 2288 2961 source = "registry+https://github.com/rust-lang/crates.io-index" 2289 - checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 2962 + checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 2290 2963 dependencies = [ 2291 2964 "serde_derive", 2292 2965 ] ··· 2302 2975 2303 2976 [[package]] 2304 2977 name = "serde_derive" 2305 - version = "1.0.218" 2978 + version = "1.0.219" 2306 2979 source = "registry+https://github.com/rust-lang/crates.io-index" 2307 - checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 2980 + checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 2308 2981 dependencies = [ 2309 2982 "proc-macro2", 2310 2983 "quote", ··· 2395 3068 ] 2396 3069 2397 3070 [[package]] 3071 + name = "signature" 3072 + version = "2.2.0" 3073 + source = "registry+https://github.com/rust-lang/crates.io-index" 3074 + checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 3075 + dependencies = [ 3076 + "digest", 3077 + "rand_core 0.6.4", 3078 + ] 3079 + 3080 + [[package]] 2398 3081 name = "slab" 2399 3082 version = "0.4.9" 2400 3083 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2420 3103 ] 2421 3104 2422 3105 [[package]] 3106 + name = "spin" 3107 + version = "0.9.8" 3108 + source = "registry+https://github.com/rust-lang/crates.io-index" 3109 + checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 3110 + dependencies = [ 3111 + "lock_api", 3112 + ] 3113 + 3114 + [[package]] 2423 3115 name = "stable_deref_trait" 2424 3116 version = "1.2.0" 2425 3117 source = "registry+https://github.com/rust-lang/crates.io-index" 2426 3118 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 3119 + 3120 + [[package]] 3121 + name = "strsim" 3122 + version = "0.11.1" 3123 + source = "registry+https://github.com/rust-lang/crates.io-index" 3124 + checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2427 3125 2428 3126 [[package]] 2429 3127 name = "subtle" ··· 2554 3252 ] 2555 3253 2556 3254 [[package]] 3255 + name = "tinyvec" 3256 + version = "1.9.0" 3257 + source = "registry+https://github.com/rust-lang/crates.io-index" 3258 + checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" 3259 + dependencies = [ 3260 + "tinyvec_macros", 3261 + ] 3262 + 3263 + [[package]] 3264 + name = "tinyvec_macros" 3265 + version = "0.1.1" 3266 + source = "registry+https://github.com/rust-lang/crates.io-index" 3267 + checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 3268 + 3269 + [[package]] 2557 3270 name = "tokio" 2558 - version = "1.44.0" 3271 + version = "1.44.1" 2559 3272 source = "registry+https://github.com/rust-lang/crates.io-index" 2560 - checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" 3273 + checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" 2561 3274 dependencies = [ 2562 3275 "backtrace", 2563 3276 "bytes", ··· 2567 3280 "pin-project-lite", 2568 3281 "signal-hook-registry", 2569 3282 "socket2", 3283 + "tokio-macros", 2570 3284 "windows-sys 0.52.0", 2571 3285 ] 2572 3286 2573 3287 [[package]] 3288 + name = "tokio-macros" 3289 + version = "2.5.0" 3290 + source = "registry+https://github.com/rust-lang/crates.io-index" 3291 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 3292 + dependencies = [ 3293 + "proc-macro2", 3294 + "quote", 3295 + "syn", 3296 + ] 3297 + 3298 + [[package]] 2574 3299 name = "tokio-native-tls" 2575 3300 version = "0.3.1" 2576 3301 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2581 3306 ] 2582 3307 2583 3308 [[package]] 3309 + name = "tokio-rustls" 3310 + version = "0.24.1" 3311 + source = "registry+https://github.com/rust-lang/crates.io-index" 3312 + checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" 3313 + dependencies = [ 3314 + "rustls", 3315 + "tokio", 3316 + ] 3317 + 3318 + [[package]] 3319 + name = "tokio-tungstenite" 3320 + version = "0.20.1" 3321 + source = "registry+https://github.com/rust-lang/crates.io-index" 3322 + checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" 3323 + dependencies = [ 3324 + "futures-util", 3325 + "log", 3326 + "rustls", 3327 + "rustls-native-certs", 3328 + "tokio", 3329 + "tokio-rustls", 3330 + "tungstenite", 3331 + "webpki-roots", 3332 + ] 3333 + 3334 + [[package]] 2584 3335 name = "tokio-util" 2585 - version = "0.7.13" 3336 + version = "0.7.14" 2586 3337 source = "registry+https://github.com/rust-lang/crates.io-index" 2587 - checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 3338 + checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" 2588 3339 dependencies = [ 2589 3340 "bytes", 2590 3341 "futures-core", ··· 2628 3379 dependencies = [ 2629 3380 "log", 2630 3381 "pin-project-lite", 3382 + "tracing-attributes", 2631 3383 "tracing-core", 2632 3384 ] 2633 3385 2634 3386 [[package]] 3387 + name = "tracing-attributes" 3388 + version = "0.1.28" 3389 + source = "registry+https://github.com/rust-lang/crates.io-index" 3390 + checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 3391 + dependencies = [ 3392 + "proc-macro2", 3393 + "quote", 3394 + "syn", 3395 + ] 3396 + 3397 + [[package]] 2635 3398 name = "tracing-core" 2636 3399 version = "0.1.33" 2637 3400 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2688 3451 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2689 3452 2690 3453 [[package]] 3454 + name = "tungstenite" 3455 + version = "0.20.1" 3456 + source = "registry+https://github.com/rust-lang/crates.io-index" 3457 + checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" 3458 + dependencies = [ 3459 + "byteorder", 3460 + "bytes", 3461 + "data-encoding", 3462 + "http 0.2.12", 3463 + "httparse", 3464 + "log", 3465 + "rand 0.8.5", 3466 + "rustls", 3467 + "sha1", 3468 + "thiserror", 3469 + "url", 3470 + "utf-8", 3471 + ] 3472 + 3473 + [[package]] 2691 3474 name = "typenum" 2692 3475 version = "1.18.0" 2693 3476 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2728 3511 checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" 2729 3512 2730 3513 [[package]] 3514 + name = "untrusted" 3515 + version = "0.9.0" 3516 + source = "registry+https://github.com/rust-lang/crates.io-index" 3517 + checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 3518 + 3519 + [[package]] 2731 3520 name = "url" 2732 3521 version = "2.5.4" 2733 3522 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2737 3526 "idna", 2738 3527 "percent-encoding", 2739 3528 ] 3529 + 3530 + [[package]] 3531 + name = "utf-8" 3532 + version = "0.7.6" 3533 + source = "registry+https://github.com/rust-lang/crates.io-index" 3534 + checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 2740 3535 2741 3536 [[package]] 2742 3537 name = "utf16_iter" ··· 2905 3700 ] 2906 3701 2907 3702 [[package]] 3703 + name = "webpki-roots" 3704 + version = "0.25.4" 3705 + source = "registry+https://github.com/rust-lang/crates.io-index" 3706 + checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" 3707 + 3708 + [[package]] 3709 + name = "widestring" 3710 + version = "1.2.0" 3711 + source = "registry+https://github.com/rust-lang/crates.io-index" 3712 + checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" 3713 + 3714 + [[package]] 2908 3715 name = "winapi" 2909 3716 version = "0.3.9" 2910 3717 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2928 3735 2929 3736 [[package]] 2930 3737 name = "windows" 3738 + version = "0.52.0" 3739 + source = "registry+https://github.com/rust-lang/crates.io-index" 3740 + checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" 3741 + dependencies = [ 3742 + "windows-core 0.52.0", 3743 + "windows-targets 0.52.6", 3744 + ] 3745 + 3746 + [[package]] 3747 + name = "windows" 2931 3748 version = "0.58.0" 2932 3749 source = "registry+https://github.com/rust-lang/crates.io-index" 2933 3750 checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" 2934 3751 dependencies = [ 2935 3752 "windows-core 0.58.0", 2936 - "windows-targets", 3753 + "windows-targets 0.52.6", 2937 3754 ] 2938 3755 2939 3756 [[package]] ··· 2942 3759 source = "registry+https://github.com/rust-lang/crates.io-index" 2943 3760 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2944 3761 dependencies = [ 2945 - "windows-targets", 3762 + "windows-targets 0.52.6", 2946 3763 ] 2947 3764 2948 3765 [[package]] ··· 2955 3772 "windows-interface", 2956 3773 "windows-result", 2957 3774 "windows-strings", 2958 - "windows-targets", 3775 + "windows-targets 0.52.6", 2959 3776 ] 2960 3777 2961 3778 [[package]] ··· 2994 3811 dependencies = [ 2995 3812 "windows-result", 2996 3813 "windows-strings", 2997 - "windows-targets", 3814 + "windows-targets 0.52.6", 2998 3815 ] 2999 3816 3000 3817 [[package]] ··· 3003 3820 source = "registry+https://github.com/rust-lang/crates.io-index" 3004 3821 checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 3005 3822 dependencies = [ 3006 - "windows-targets", 3823 + "windows-targets 0.52.6", 3007 3824 ] 3008 3825 3009 3826 [[package]] ··· 3013 3830 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 3014 3831 dependencies = [ 3015 3832 "windows-result", 3016 - "windows-targets", 3833 + "windows-targets 0.52.6", 3834 + ] 3835 + 3836 + [[package]] 3837 + name = "windows-sys" 3838 + version = "0.48.0" 3839 + source = "registry+https://github.com/rust-lang/crates.io-index" 3840 + checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 3841 + dependencies = [ 3842 + "windows-targets 0.48.5", 3017 3843 ] 3018 3844 3019 3845 [[package]] ··· 3022 3848 source = "registry+https://github.com/rust-lang/crates.io-index" 3023 3849 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 3024 3850 dependencies = [ 3025 - "windows-targets", 3851 + "windows-targets 0.52.6", 3026 3852 ] 3027 3853 3028 3854 [[package]] ··· 3031 3857 source = "registry+https://github.com/rust-lang/crates.io-index" 3032 3858 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 3033 3859 dependencies = [ 3034 - "windows-targets", 3860 + "windows-targets 0.52.6", 3861 + ] 3862 + 3863 + [[package]] 3864 + name = "windows-targets" 3865 + version = "0.48.5" 3866 + source = "registry+https://github.com/rust-lang/crates.io-index" 3867 + checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 3868 + dependencies = [ 3869 + "windows_aarch64_gnullvm 0.48.5", 3870 + "windows_aarch64_msvc 0.48.5", 3871 + "windows_i686_gnu 0.48.5", 3872 + "windows_i686_msvc 0.48.5", 3873 + "windows_x86_64_gnu 0.48.5", 3874 + "windows_x86_64_gnullvm 0.48.5", 3875 + "windows_x86_64_msvc 0.48.5", 3035 3876 ] 3036 3877 3037 3878 [[package]] ··· 3040 3881 source = "registry+https://github.com/rust-lang/crates.io-index" 3041 3882 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 3042 3883 dependencies = [ 3043 - "windows_aarch64_gnullvm", 3044 - "windows_aarch64_msvc", 3045 - "windows_i686_gnu", 3884 + "windows_aarch64_gnullvm 0.52.6", 3885 + "windows_aarch64_msvc 0.52.6", 3886 + "windows_i686_gnu 0.52.6", 3046 3887 "windows_i686_gnullvm", 3047 - "windows_i686_msvc", 3048 - "windows_x86_64_gnu", 3049 - "windows_x86_64_gnullvm", 3050 - "windows_x86_64_msvc", 3888 + "windows_i686_msvc 0.52.6", 3889 + "windows_x86_64_gnu 0.52.6", 3890 + "windows_x86_64_gnullvm 0.52.6", 3891 + "windows_x86_64_msvc 0.52.6", 3051 3892 ] 3052 3893 3053 3894 [[package]] 3054 3895 name = "windows_aarch64_gnullvm" 3896 + version = "0.48.5" 3897 + source = "registry+https://github.com/rust-lang/crates.io-index" 3898 + checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 3899 + 3900 + [[package]] 3901 + name = "windows_aarch64_gnullvm" 3055 3902 version = "0.52.6" 3056 3903 source = "registry+https://github.com/rust-lang/crates.io-index" 3057 3904 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 3058 3905 3059 3906 [[package]] 3060 3907 name = "windows_aarch64_msvc" 3908 + version = "0.48.5" 3909 + source = "registry+https://github.com/rust-lang/crates.io-index" 3910 + checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 3911 + 3912 + [[package]] 3913 + name = "windows_aarch64_msvc" 3061 3914 version = "0.52.6" 3062 3915 source = "registry+https://github.com/rust-lang/crates.io-index" 3063 3916 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 3917 + 3918 + [[package]] 3919 + name = "windows_i686_gnu" 3920 + version = "0.48.5" 3921 + source = "registry+https://github.com/rust-lang/crates.io-index" 3922 + checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 3064 3923 3065 3924 [[package]] 3066 3925 name = "windows_i686_gnu" ··· 3076 3935 3077 3936 [[package]] 3078 3937 name = "windows_i686_msvc" 3938 + version = "0.48.5" 3939 + source = "registry+https://github.com/rust-lang/crates.io-index" 3940 + checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 3941 + 3942 + [[package]] 3943 + name = "windows_i686_msvc" 3079 3944 version = "0.52.6" 3080 3945 source = "registry+https://github.com/rust-lang/crates.io-index" 3081 3946 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 3082 3947 3083 3948 [[package]] 3084 3949 name = "windows_x86_64_gnu" 3950 + version = "0.48.5" 3951 + source = "registry+https://github.com/rust-lang/crates.io-index" 3952 + checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 3953 + 3954 + [[package]] 3955 + name = "windows_x86_64_gnu" 3085 3956 version = "0.52.6" 3086 3957 source = "registry+https://github.com/rust-lang/crates.io-index" 3087 3958 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 3088 3959 3089 3960 [[package]] 3090 3961 name = "windows_x86_64_gnullvm" 3962 + version = "0.48.5" 3963 + source = "registry+https://github.com/rust-lang/crates.io-index" 3964 + checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 3965 + 3966 + [[package]] 3967 + name = "windows_x86_64_gnullvm" 3091 3968 version = "0.52.6" 3092 3969 source = "registry+https://github.com/rust-lang/crates.io-index" 3093 3970 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 3094 3971 3095 3972 [[package]] 3096 3973 name = "windows_x86_64_msvc" 3974 + version = "0.48.5" 3975 + source = "registry+https://github.com/rust-lang/crates.io-index" 3976 + checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 3977 + 3978 + [[package]] 3979 + name = "windows_x86_64_msvc" 3097 3980 version = "0.52.6" 3098 3981 source = "registry+https://github.com/rust-lang/crates.io-index" 3099 3982 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 3100 3983 3101 3984 [[package]] 3985 + name = "winnow" 3986 + version = "0.7.4" 3987 + source = "registry+https://github.com/rust-lang/crates.io-index" 3988 + checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" 3989 + dependencies = [ 3990 + "memchr", 3991 + ] 3992 + 3993 + [[package]] 3994 + name = "winreg" 3995 + version = "0.50.0" 3996 + source = "registry+https://github.com/rust-lang/crates.io-index" 3997 + checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" 3998 + dependencies = [ 3999 + "cfg-if", 4000 + "windows-sys 0.48.0", 4001 + ] 4002 + 4003 + [[package]] 3102 4004 name = "wit-bindgen-rt" 3103 4005 version = "0.33.0" 3104 4006 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3150 4052 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 3151 4053 dependencies = [ 3152 4054 "byteorder", 3153 - "zerocopy-derive", 4055 + "zerocopy-derive 0.7.35", 4056 + ] 4057 + 4058 + [[package]] 4059 + name = "zerocopy" 4060 + version = "0.8.24" 4061 + source = "registry+https://github.com/rust-lang/crates.io-index" 4062 + checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 4063 + dependencies = [ 4064 + "zerocopy-derive 0.8.24", 3154 4065 ] 3155 4066 3156 4067 [[package]] ··· 3165 4076 ] 3166 4077 3167 4078 [[package]] 4079 + name = "zerocopy-derive" 4080 + version = "0.8.24" 4081 + source = "registry+https://github.com/rust-lang/crates.io-index" 4082 + checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 4083 + dependencies = [ 4084 + "proc-macro2", 4085 + "quote", 4086 + "syn", 4087 + ] 4088 + 4089 + [[package]] 3168 4090 name = "zerofrom" 3169 4091 version = "0.1.6" 3170 4092 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3183 4105 "quote", 3184 4106 "syn", 3185 4107 "synstructure", 4108 + ] 4109 + 4110 + [[package]] 4111 + name = "zeroize" 4112 + version = "1.8.1" 4113 + source = "registry+https://github.com/rust-lang/crates.io-index" 4114 + checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 4115 + dependencies = [ 4116 + "serde", 3186 4117 ] 3187 4118 3188 4119 [[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.1.1" # pyt the latest version here 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.
+1070 -5
README.md
··· 1 - # Dev commands 1 + 2 + 3 + # !!!!!!!!!!!!!!!Squash before going public!!!!!!!!!! 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + Originally taken 13 + from [bluesky-social/atproto-website](https://github.com/bluesky-social/atproto-website/blob/dbcd70ced53078579c7e5b015a26db295b7a7807/src/app/%5Blocale%5D/guides/applications/en.mdx) 14 + 15 + > [!NOTE] 16 + > ***This tutorial is based off of the original quick start guide found [here](https://atproto.com/guides/applications). 17 + > The goal is to follow as closely to the original as possible, expect for one small change. It's in Rust 🦀. 18 + > All credit goes to the maintainers of the original project and tutorial. This was made to help you get started with 19 + > 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.*** 20 + 21 + # Quick start guide to building applications on AT Protocol 22 + 23 + [Find the source code on GitHub](https://github.com/fatfingers23/rusty_statusphere_example_app) 24 + 25 + In this guide, we're going to build a simple multi-user app that publishes your current "status" as an emoji. Our 26 + application will look like this: 27 + 28 + ![Picture of the application](./images/cover.png) 29 + 30 + We will cover how to: 31 + 32 + - Signin via OAuth 33 + - Fetch information about users (profiles) 34 + - Listen to the network firehose for new data via the [Jetstream](https://docs.bsky.app/blog/jetstream) 35 + - Publish data on the user's account using a custom schema 36 + 37 + We're going to keep this light so you can quickly wrap your head around ATProto. There will be links with more 38 + information about each step. 39 + 40 + ## Introduction 41 + 42 + Data in the Atmosphere is stored on users' personal repos. It's almost like each user has their own website. Our goal is 43 + to aggregate data from the users into our SQLite DB. 44 + 45 + Think of our app like a Google. If Google's job was to say which emoji each website had under `/status.json`, then it 46 + would show something like: 47 + 48 + - `nytimes.com` is feeling 📰 according to `https://nytimes.com/status.json` 49 + - `bsky.app` is feeling 🦋 according to `https://bsky.app/status.json` 50 + - `reddit.com` is feeling 🤓 according to `https://reddit.com/status.json` 51 + 52 + The Atmosphere works the same way, except we're going to check `at://` instead of `https://`. Each user has a data repo 53 + under an `at://` URL. We'll crawl all the user data repos in the Atmosphere for all the "status.json" records and 54 + aggregate them into our SQLite database. 55 + 56 + > `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 57 + > the features we'll be using in this tutorial. 58 + 59 + ## Step 1. Starting with our Actix Web app 60 + 61 + Start by cloning the repo and installing packages. 62 + 63 + ```bash 64 + git clone git@github.com:fatfingers23/rusty_statusphere_example_app.git 65 + cd rusty_statusphere_example_app 66 + cp .env.template .env 67 + cargo run 68 + # Navigate to http://127.0.0.1:8080 69 + ``` 70 + 71 + 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 72 + we're managing with [async-sqlite](https://crates.io/crates/async-sqlite). 73 + 74 + Our starting stack: 75 + 76 + - [Rust](https://www.rust-lang.org/tools/install) 77 + - Rust web server ([Actix Web](https://actix.rs/)) 78 + - SQLite database ([async-sqlite](https://crates.io/crates/async-sqlite)) 79 + - HTML Templating ([askama](https://crates.io/crates/askama)) 80 + 81 + > [!NOTE] 82 + > Along with the above, we are also using a couple of community maintained projects for using rust with the ATProtocol. 83 + > Since these are community maintained I have also linked sponsor links for the maintainers and _highly_ recommend you to 84 + > think 85 + > about sponsoring them. 86 + > Thanks to their work and projects, we are able to create Rust applications in the Atmosphere. 87 + > - ATProtocol client and OAuth 88 + with [atrium](https://github.com/atrium-rs/atrium) - [sponsor sugyan](https://github.com/sponsors/sugyan) 89 + > - Jetstream consumer 90 + with [rocketman](https://crates.io/crates/rocketman)- [buy natalie a coffee](https://ko-fi.com/uxieq) 91 + 92 + With each step we'll explain how our Web app taps into the Atmosphere. Refer to the codebase for more detailed code 93 + &mdash; again, this tutorial is going to keep it light and quick to digest. 94 + 95 + ## Step 2. Signing in with OAuth 96 + 97 + When somebody logs into our app, they'll give us read & write access to their personal `at://` repo. We'll use that to 98 + write the status json record. 99 + 100 + We're going to accomplish this using OAuth ([spec](https://github.com/bluesky-social/proposals/tree/main/0004-oauth)). 101 + Most of the OAuth flows are going to be handled for us using 102 + the [atrium-oauth](https://crates.io/crates/atrium-oauth) 103 + crate. This is the arrangement we're aiming toward: 104 + 105 + ![A diagram of the OAuth elements](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-oauth.5ebec062.png&w=750&q=75) 106 + 107 + When the user logs in, the OAuth client will create a new session with their repo server and give us read/write access 108 + along with basic user info. 109 + 110 + ![A screenshot of the login UI](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp-login.83cd693f.png&w=640&q=75) 111 + 112 + Our login page just asks the user for their "handle," which is the domain name associated with their account. 113 + For [Bluesky](https://bsky.app) users, these tend to look like `alice.bsky.social`, but they can be any kind of domain ( 114 + eg `alice.com`). 115 + 116 + ```html 117 + <!-- templates/login.html --> 118 + <form action="/login" method="post" class="login-form"> 119 + <input 120 + type="text" 121 + name="handle" 122 + placeholder="Enter your handle (eg alice.bsky.social)" 123 + required 124 + /> 125 + <button type="submit">Log in</button> 126 + </form> 127 + ``` 128 + 129 + When they submit the form, we tell our OAuth client to initiate the authorization flow and then redirect the user to 130 + their server to complete the process. 131 + 132 + ```rust 133 + /** ./src/main.rs **/ 134 + /// Login endpoint 135 + #[post("/login")] 136 + async fn login_post( 137 + request: HttpRequest, 138 + params: web::Form<LoginForm>, 139 + oauth_client: web::Data<OAuthClientType>, 140 + ) -> HttpResponse { 141 + // This will act the same as the js method isValidHandle 142 + match atrium_api::types::string::Handle::new(params.handle.clone()) { 143 + Ok(handle) => { 144 + // Initiates the OAuth flow 145 + let oauth_url = oauth_client 146 + .authorize( 147 + &handle, 148 + AuthorizeOptions { 149 + scopes: vec![ 150 + Scope::Known(KnownScope::Atproto), 151 + Scope::Known(KnownScope::TransitionGeneric), 152 + ], 153 + ..Default::default() 154 + }, 155 + ) 156 + .await; 157 + match oauth_url { 158 + Ok(url) => Redirect::to(url) 159 + .see_other() 160 + .respond_to(&request) 161 + .map_into_boxed_body(), 162 + Err(err) => { 163 + log::error!("Error: {err}"); 164 + let html = LoginTemplate { 165 + title: "Log in", 166 + error: Some("OAuth error"), 167 + }; 168 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 169 + } 170 + } 171 + } 172 + Err(err) => { 173 + let html: LoginTemplate<'_> = LoginTemplate { 174 + title: "Log in", 175 + error: Some(err), 176 + }; 177 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 178 + } 179 + } 180 + } 181 + ``` 182 + 183 + This is the same kind of SSO flow that Google or GitHub uses. The user will be asked for their password, then asked to 184 + confirm the session with your application. 185 + 186 + When that finishes, the user will be sent back to `/oauth/callback` on our Web app. The OAuth client will store the 187 + access tokens for the user's server, and then we attach their account's [DID](https://atproto.com/specs/did) to the 188 + cookie-session. 189 + 190 + ```rust 191 + /** ./src/main.rs **/ 192 + /// OAuth callback endpoint to complete session creation 193 + #[get("/oauth/callback")] 194 + async fn oauth_callback( 195 + request: HttpRequest, 196 + params: web::Query<CallbackParams>, 197 + oauth_client: web::Data<OAuthClientType>, 198 + session: Session, 199 + ) -> HttpResponse { 200 + // Store the credentials 201 + match oauth_client.callback(params.into_inner()).await { 202 + Ok((bsky_session, _)) => { 203 + let agent = Agent::new(bsky_session); 204 + match agent.did().await { 205 + Some(did) => { 206 + //Attach the account DID to our user via a cookie 207 + session.insert("did", did).unwrap(); 208 + Redirect::to("/") 209 + .see_other() 210 + .respond_to(&request) 211 + .map_into_boxed_body() 212 + } 213 + None => { 214 + let html = ErrorTemplate { 215 + title: "Log in", 216 + error: "The OAuth agent did not return a DID. My try relogging in.", 217 + }; 218 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 219 + } 220 + } 221 + } 222 + Err(err) => { 223 + log::error!("Error: {err}"); 224 + let html = ErrorTemplate { 225 + title: "Log in", 226 + error: "OAuth error, check the logs", 227 + }; 228 + HttpResponse::Ok().body(html.render().expect("template should be valid")) 229 + } 230 + } 231 + } 232 + ``` 233 + 234 + 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. 235 + 236 + ## Step 3. Fetching the user's profile 237 + 238 + Why don't we learn something about our user? In [Bluesky](https://bsky.app), users publish a "profile" record which 239 + looks like this: 240 + 241 + ```rust 242 + pub struct ProfileViewDetailedData { 243 + pub display_name: Option<String>, // a human friendly name 244 + pub description: Option<String>, // a short bio 245 + pub avatar: Option<String>, // small profile picture 246 + pub banner: Option<String>, // banner image to put on profiles 247 + pub created_at: Option<String> // declared time this profile data was added 248 + // ... 249 + } 250 + ``` 251 + 252 + You can examine this record directly using [atproto-browser.vercel.app](https://atproto-browser.vercel.app). For 253 + 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). 254 + 255 + > [!NOTE] 256 + > In the original tutorial `agent.com.atproto.repo.getRecord` is used, which is 257 + > this [method](https://docs.rs/atrium-api/latest/atrium_api/com/atproto/repo/get_record/index.html) in atrium-api. 258 + > For simplicity we are 259 + > using [agent.api.app.bsky.actor.get_profile](https://docs.rs/atrium-api/latest/atrium_api/app/bsky/actor/get_profile/index.html). 260 + > The original text found here has been moved to [Step 4. Reading & writing records](#step-4-reading--writing-records) 261 + > since it makes more sense in that context. 262 + 263 + We're going to use the [Agent](https://crates.io/crates/atrium-oauth) associated with the 264 + user's OAuth session to fetch this record. 265 + 266 + Let's update our homepage to fetch this profile record: 267 + 268 + ```rust 269 + /** ./src/main.rs **/ 270 + /// Homepage 271 + #[get("/")] 272 + async fn home( 273 + _req: HttpRequest, 274 + session: Session, 275 + oauth_client: web::Data<OAuthClientType>, 276 + db_pool: web::Data<Pool>, 277 + handle_resolver: web::Data<HandleResolver>, 278 + ) -> Result<impl Responder> { 279 + const TITLE: &str = "Home"; 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 + match oauth_client.restore(&did).await { 286 + Ok(session) => { 287 + let agent = Agent::new(session); 288 + 289 + // Fetch additional information about the logged-in user 290 + let profile = agent 291 + .api 292 + .app 293 + .bsky 294 + .actor 295 + .get_profile( 296 + atrium_api::app::bsky::actor::get_profile::ParametersData { 297 + actor: atrium_api::types::string::AtIdentifier::Did(did), 298 + }.into(), 299 + ) 300 + .await; 301 + 302 + // Serve the logged-in view 303 + let html = HomeTemplate { 304 + title: TITLE, 305 + status_options: &STATUS_OPTIONS, 306 + profile: match profile { 307 + Ok(profile) => { 308 + let profile_data = Profile { 309 + did: profile.did.to_string(), 310 + display_name: profile.display_name.clone(), 311 + }; 312 + Some(profile_data) 313 + } 314 + Err(err) => { 315 + log::error!("Error accessing profile: {err}"); 316 + None 317 + } 318 + }, 319 + }.render().expect("template should be valid"); 320 + 321 + Ok(web::Html::new(html)) 322 + } 323 + Err(err) => { 324 + //Unset the session 325 + session.remove("did"); 326 + log::error!("Error restoring session: {err}"); 327 + let error_html = ErrorTemplate { 328 + title: TITLE, 329 + error: "Was an error resuming the session, please check the logs.", 330 + }.render().expect("template should be valid"); 331 + 332 + Ok(web::Html::new(error_html)) 333 + } 334 + } 335 + } 336 + None => { 337 + // Serve the logged-out view 338 + let html = HomeTemplate { 339 + title: TITLE, 340 + status_options: &STATUS_OPTIONS, 341 + profile: None, 342 + }.render().expect("template should be valid"); 343 + 344 + Ok(web::Html::new(html)) 345 + } 346 + } 347 + } 348 + ``` 349 + 350 + With that data, we can give a nice personalized welcome banner for our user: 351 + 352 + ![A screenshot of the banner image](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fapp-banner.1e92c654.png&w=640&q=75) 353 + 354 + ```html 355 + <!-- templates/home.html --> 356 + <div class="card"> 357 + {% if let Some(Profile {did, display_name}) = profile %} 358 + <form action="/logout" method="post" class="session-form"> 359 + <div> 360 + Hi, 361 + {% if let Some(display_name) = display_name %} 362 + <strong>{{display_name}}</strong> 363 + {% else %} 364 + <strong>friend</strong> 365 + {% endif %}. 366 + What's your status today?? 367 + </div> 368 + <div> 369 + <button type="submit">Log out</button> 370 + </div> 371 + </form> 372 + {% else %} 373 + <div class="session-form"> 374 + <div><a href="/login">Log in</a> to set your status!</div> 375 + <div> 376 + <a href="/login" class="button">Log in</a> 377 + </div> 378 + </div> 379 + {% endif %} 380 + </div> 381 + ``` 382 + 383 + ## Step 4. Reading & writing records 384 + 385 + You can think of the user repositories as collections of JSON records: 386 + 387 + !["A diagram of a repository"](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-repo.4a34005b.png&w=750&q=75) 388 + 389 + When asking for a record, we provide three pieces of information. 390 + 391 + - **repo** The [DID](https://atproto.com/specs/did) which identifies the user, 392 + - **collection** The collection name, and 393 + - **rkey** The record key 394 + 395 + We'll explain the collection name shortly. Record keys are strings 396 + with [some restrictions](https://atproto.com/specs/record-key#record-key-syntax) and a couple of common patterns. The 397 + `"self"` pattern is used when a collection is expected to only contain one record which describes the user. 398 + 399 + Let's look again at how we read the "profile" record: 400 + 401 + ```rust 402 + fn example_get_record() { 403 + let get_result = agent 404 + .api 405 + .com 406 + .atproto 407 + .repo 408 + .get_record( 409 + atrium_api::com::atproto::repo::get_record::ParametersData { 410 + cid: None, 411 + collection: "app.bsky.actor.profile" // The collection 412 + .parse() 413 + .unwrap(), 414 + repo: did.into(), // The user 415 + rkey: "self".parse().unwrap(), // The record key 416 + } 417 + .into(), 418 + ) 419 + .await; 420 + } 421 + 422 + ``` 423 + 424 + We write records using a similar API. Since our goal is to write "status" records, let's look at how that will happen: 425 + 426 + ```rust 427 + fn example_create_record() { 428 + let did = atrium_api::types::string::Did::new(did_string.clone()).unwrap(); 429 + let agent = Agent::new(session); 430 + 431 + let status: Unknown = serde_json::from_str( 432 + format!( 433 + r#"{{"$type":"xyz.statusphere.status","status":"{}","createdAt":"{}"}}"#, 434 + form.status, 435 + Datetime::now().as_str() 436 + ) 437 + .as_str(), 438 + ).unwrap(); 439 + 440 + let create_result = agent 441 + .api 442 + .com 443 + .atproto 444 + .repo 445 + .create_record( 446 + atrium_api::com::atproto::repo::create_record::InputData { 447 + collection: Status::NSID.parse().unwrap(), // The collection 448 + repo: did.clone().into(), // The user 449 + rkey: None, // The record key, auto creates with None 450 + record: status, // The record from a strong type 451 + swap_commit: None, 452 + validate: None, 453 + } 454 + .into(), 455 + ) 456 + .await; 457 + } 458 + ``` 459 + 460 + Our `POST /status` route is going to use this API to publish the user's status to their repo. 461 + 462 + ```rust 463 + /// "Set status" Endpoint 464 + #[post("/status")] 465 + async fn status( 466 + request: HttpRequest, 467 + session: Session, 468 + oauth_client: web::Data<OAuthClientType>, 469 + db_pool: web::Data<Pool>, 470 + form: web::Form<StatusForm>, 471 + ) -> HttpResponse { 472 + const TITLE: &str = "Home"; 473 + 474 + // If the user is signed in, get an agent which communicates with their server 475 + match session.get::<String>("did").unwrap_or(None) { 476 + Some(did_string) => { 477 + let did = atrium_api::types::string::Did::new(did_string.clone()) 478 + .expect("failed to parse did"); 479 + match oauth_client.restore(&did).await { 480 + Ok(session) => { 481 + let agent = Agent::new(session); 482 + 483 + // Construct their status record 484 + let status: Unknown = serde_json::from_str( 485 + format!( 486 + r#"{{"$type":"xyz.statusphere.status","status":"{}","createdAt":"{}"}}"#, 487 + form.status, 488 + Datetime::now().as_str() 489 + ) 490 + .as_str(), 491 + ).unwrap(); 492 + 493 + // Write the status record to the user's repository 494 + let create_result = agent 495 + .api 496 + .com 497 + .atproto 498 + .repo 499 + .create_record( 500 + atrium_api::com::atproto::repo::create_record::InputData { 501 + collection: "xyz.statusphere.status".parse().unwrap(), 502 + repo: did.clone().into(), 503 + rkey: None, 504 + record: status, 505 + swap_commit: None, 506 + validate: None, 507 + } 508 + .into(), 509 + ) 510 + .await; 511 + 512 + match create_result { 513 + Ok(_) => Redirect::to("/") 514 + .see_other() 515 + .respond_to(&request) 516 + .map_into_boxed_body(), 517 + Err(err) => { 518 + log::error!("Error creating status: {err}"); 519 + let error_html = ErrorTemplate { 520 + title: TITLE, 521 + error: "Was an error creating the status, please check the logs.", 522 + } 523 + .render() 524 + .expect("template should be valid"); 525 + HttpResponse::Ok().body(error_html) 526 + } 527 + } 528 + } 529 + Err(err) => { 530 + //Unset the session 531 + session.remove("did"); 532 + log::error!( 533 + "Error restoring session, we are removing the session from the cookie: {err}" 534 + ); 535 + let error_html = ErrorTemplate { 536 + title: TITLE, 537 + error: "Was an error resuming the session, please check the logs.", 538 + } 539 + .render() 540 + .expect("template should be valid"); 541 + HttpResponse::Ok().body(error_html) 542 + } 543 + } 544 + } 545 + None => { 546 + let error_template = ErrorTemplate { 547 + title: "Error", 548 + error: "You must be logged in to create a status.", 549 + } 550 + .render() 551 + .expect("template should be valid"); 552 + HttpResponse::Ok().body(error_template) 553 + } 554 + } 555 + } 556 + ``` 557 + 558 + Now in our homepage we can list out the status buttons: 559 + 560 + ```html 561 + <!-- templates/home.html --> 562 + <form action="/status" method="post" class="status-options"> 563 + {% for status in status_options %} 564 + <button 565 + class="{% if let Some(my_status) = my_status %} {%if my_status == status %} status-option selected {% else %} status-option {% endif %} {% else %} status-option {%endif%} " 566 + name="status" value="{{status}}"> 567 + {{status}} 568 + </button> 569 + {% endfor %} 570 + </form> 571 + ``` 572 + 573 + And here we are! 574 + 575 + ![A screenshot of the app's status options"](./images/emojis.png) 576 + 577 + ## Step 5. Creating a custom "status" schema 578 + 579 + Repo collections are typed, meaning that they have a defined schema. The `app.bsky.actor.profile` type 580 + definition [can be found here](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/profile.json). 581 + 582 + Anybody can create a new schema using the [Lexicon](https://atproto.com/specs/lexicon) language, which is very similar 583 + to [JSON-Schema](http://json-schema.org/). The schemas use [reverse-DNS IDs](https://atproto.com/specs/nsid) which 584 + indicate ownership. In this demo app we're going to use `xyz.statusphere` which we registered specifically for this 585 + project (aka statusphere.xyz). 586 + 587 + > ### Why create a schema? 588 + > 589 + > Schemas help other applications understand the data your app is creating. By publishing your schemas, you make it 590 + > easier for other application authors to publish data in a format your app will recognize and handle. 591 + 592 + Let's create our schema in the `/lexicons` folder of our codebase. You 593 + can [read more about how to define schemas here](https://atproto.com/guides/lexicon). 594 + 595 + ```json 596 + /** lexicons/status.json **/ 597 + { 598 + "lexicon": 1, 599 + "id": "xyz.statusphere.status", 600 + "defs": { 601 + "main": { 602 + "type": "record", 603 + "key": "tid", 604 + "record": { 605 + "type": "object", 606 + "required": [ 607 + "status", 608 + "createdAt" 609 + ], 610 + "properties": { 611 + "status": { 612 + "type": "string", 613 + "minLength": 1, 614 + "maxGraphemes": 1, 615 + "maxLength": 32 616 + }, 617 + "createdAt": { 618 + "type": "string", 619 + "format": "datetime" 620 + } 621 + } 622 + } 623 + } 624 + } 625 + } 626 + ``` 627 + 628 + Now let's run some code-generation using our schema: 629 + 630 + > [!NOTE] 631 + > For generating schemas, we are going to 632 + > use [esquema-cli](https://github.com/fatfingers23/esquema?tab=readme-ov-file) 633 + > (Which is a tool I've created from a fork of atrium's codegen). 634 + > This can be installed by running this command 635 + `cargo install esquema-cli --git https://github.com/fatfingers23/esquema.git` 636 + > This is a WIP tool with bugs and missing features. But it's good enough for us to generate Rust types from the lexicon 637 + > schema. 638 + 639 + ```bash 640 + esquema-cli generate -l ./lexicons/ -o ./src/lexicons/ 641 + ``` 642 + 643 + 644 + 645 + This will produce Rust structs. Here's what that generated code looks like: 646 + 647 + ```rust 648 + /** ./src/lexicons/xyz/statusphere/status.rs **/ 649 + // @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT. 650 + //!Definitions for the `xyz.statusphere.status` namespace. 651 + use atrium_api::types::TryFromUnknown; 652 + #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] 653 + #[serde(rename_all = "camelCase")] 654 + pub struct RecordData { 655 + pub created_at: atrium_api::types::string::Datetime, 656 + pub status: String, 657 + } 658 + pub type Record = atrium_api::types::Object<RecordData>; 659 + impl From<atrium_api::types::Unknown> for RecordData { 660 + fn from(value: atrium_api::types::Unknown) -> Self { 661 + Self::try_from_unknown(value).unwrap() 662 + } 663 + } 664 + 665 + ``` 666 + 667 + > [!NOTE] 668 + > You may have noticed we do not cover the validation part like in the TypeScript version. 669 + > Esquema can validate to a point such as the data structure and if a field is there or not. 670 + > But validation of the data itself is not possible, yet. 671 + > There are plans to add it. 672 + > Maybe you would like to add it? 673 + > https://github.com/fatfingers23/esquema/issues/3 674 + 675 + Let's use that code to improve the `POST /status` route: 676 + 677 + ```rust 678 + /// "Set status" Endpoint 679 + #[post("/status")] 680 + async fn status( 681 + request: HttpRequest, 682 + session: Session, 683 + oauth_client: web::Data<OAuthClientType>, 684 + db_pool: web::Data<Pool>, 685 + form: web::Form<StatusForm>, 686 + ) -> HttpResponse { 687 + // ... 688 + let agent = Agent::new(session); 689 + //We use the new status type we generated with esquema 690 + let status: KnownRecord = lexicons::xyz::statusphere::status::RecordData { 691 + created_at: Datetime::now(), 692 + status: form.status.clone(), 693 + } 694 + .into(); 695 + 696 + // TODO no validation yet from esquema 697 + // Maybe you'd like to add it? https://github.com/fatfingers23/esquema/issues/3 698 + 699 + let create_result = agent 700 + .api 701 + .com 702 + .atproto 703 + .repo 704 + .create_record( 705 + atrium_api::com::atproto::repo::create_record::InputData { 706 + collection: Status::NSID.parse().unwrap(), 707 + repo: did.into(), 708 + rkey: None, 709 + record: status.into(), 710 + swap_commit: None, 711 + validate: None, 712 + } 713 + .into(), 714 + ) 715 + .await; 716 + // ... 717 + } 718 + ``` 719 + > [!NOTE] 720 + > You will notice the first example used a string to serialize to Unknown, you could do something similar with 721 + > a struct you create, then serialize.But I created esquema to make that easier. 722 + > With esquema you can use other provided lexicons 723 + > or ones you create to build out the data structure for your ATProtocol application. 724 + > As well as in future updates it will honor the 725 + > validation you have in the Lexicon. 726 + > Things like string should be 10 long, etc. 727 + 728 + ## Step 6. Listening to the firehose 729 + 730 + > [!IMPORTANT] 731 + > It is important to note that the original tutorial they connect directly to the firehose, but in this one we use 732 + > [rocketman](https://crates.io/crates/rocketman) to connect to the Jetstream instead. 733 + > For most use cases this is fine and usually easier when using other clients than the Bluesky provided ones. 734 + > But it is important to note there are some differences that can 735 + > be found in their introduction to Jetstream article. 736 + > https://docs.bsky.app/blog/jetstream#tradeoffs-and-use-cases 737 + 738 + So far, we have: 739 + 740 + - Logged in via OAuth 741 + - Created a custom schema 742 + - Read & written records for the logged in user 743 + 744 + Now we want to fetch the status records from other users. 745 + 746 + Remember how we referred to our app as being like Google, crawling around the repos to get their records? One advantage 747 + we have in the AT Protocol is that each repo publishes an event log of their updates. 748 + 749 + ![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) 750 + 751 + Using a [~~Relay~~ Jetstream service](https://docs.bsky.app/blog/jetstream) we can listen to an 752 + aggregated firehose of these events across all users in the network. In our case what we're looking for are valid 753 + `xyz.statusphere.status` records. 754 + 755 + ```rust 756 + /** ./src/ingester.rs **/ 757 + #[async_trait] 758 + impl LexiconIngestor for StatusSphereIngester { 759 + async fn ingest(&self, message: Event<Value>) -> anyhow::Result<()> { 760 + if let Some(commit) = &message.commit { 761 + //We manually construct the uri since jetstream does not provide it 762 + //at://{users did}/{collection: xyz.statusphere.status}{records key} 763 + let record_uri = format!("at://{}/{}/{}", message.did, commit.collection, commit.rkey); 764 + match commit.operation { 765 + Operation::Create | Operation::Update => { 766 + if let Some(record) = &commit.record { 767 + //We deserialize the record into our Rust struct 768 + let status_at_proto_record = serde_json::from_value::< 769 + lexicons::xyz::statusphere::status::RecordData, 770 + >(record.clone())?; 771 + 772 + if let Some(ref _cid) = commit.cid { 773 + // Although esquema does not have full validation yet, 774 + // if you get to this point, 775 + // You know the data structure is the same 776 + 777 + // Store the status 778 + // TODO 779 + } 780 + } 781 + } 782 + Operation::Delete => {}, 783 + } 784 + } else { 785 + return Err(anyhow!("Message has no commit")); 786 + } 787 + Ok(()) 788 + } 789 + } 790 + ``` 791 + 792 + Let's create a SQLite table to store these statuses: 793 + 794 + ```rust 795 + /** ./src/db.rs **/ 796 + // Create our statuses table 797 + pub async fn create_tables_in_database(pool: &Pool) -> Result<(), async_sqlite::Error> { 798 + pool.conn(move |conn| { 799 + conn.execute("PRAGMA foreign_keys = ON", []).unwrap(); 800 + 801 + // status 802 + conn.execute( 803 + "CREATE TABLE IF NOT EXISTS status ( 804 + uri TEXT PRIMARY KEY, 805 + authorDid TEXT NOT NULL, 806 + status TEXT NOT NULL, 807 + createdAt INTEGER NOT NULL, 808 + indexedAt INTEGER NOT NULL 809 + )", 810 + [], 811 + ) 812 + .unwrap(); 813 + 814 + // ... 815 + ``` 816 + 817 + Now we can write these statuses into our database as they arrive from the firehose: 818 + 819 + ```rust 820 + /** ./src/ingester.rs **/ 821 + // If the write is a valid status update 822 + if let Some(record) = &commit.record { 823 + let status_at_proto_record = serde_json::from_value::< 824 + lexicons::xyz::statusphere::status::RecordData, 825 + >(record.clone())?; 826 + 827 + if let Some(ref _cid) = commit.cid { 828 + // Although esquema does not have full validation yet, 829 + // if you get to this point, 830 + // You know the data structure is the same 831 + let created = status_at_proto_record.created_at.as_ref(); 832 + let right_now = chrono::Utc::now(); 833 + // We save or update the record in the db 834 + StatusFromDb { 835 + uri: record_uri, 836 + author_did: message.did.clone(), 837 + status: status_at_proto_record.status.clone(), 838 + created_at: created.to_utc(), 839 + indexed_at: right_now, 840 + handle: None, 841 + } 842 + .save_or_update(&self.db_pool) 843 + .await?; 844 + } 845 + } 846 + ``` 847 + 848 + You can almost think of information flowing in a loop: 849 + 850 + ![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) 851 + 852 + Applications write to the repo. The write events are then emitted on the firehose where they're caught by the apps and 853 + ingested into their databases. 854 + 855 + Why sync from the event log like this? Because there are other apps in the network that will write the records we're 856 + interested in. By subscribing to the event log (via the Jetstream), we ensure that we catch all the data we're interested in &mdash; 857 + including data published by other apps! 2 858 3 - tailwinds 4 - watchexec -w templates -r ~/Applications/tailwindcss --input public/css/base.css --output public/css/style.css -m 859 + ## Step 7. Listing the latest statuses 5 860 6 - watch actix 7 - watchexec -w templates -w src -r cargo run 861 + Now that we have statuses populating our SQLite, we can produce a timeline of status updates by users. We also use 862 + a [DID](https://atproto.com/specs/did)-to-handle resolver so we can show a nice username with the statuses: 863 + ```rust 864 + /** ./src/main.rs **/ 865 + // Homepage 866 + /// Home 867 + #[get("/")] 868 + async fn home( 869 + session: Session, 870 + oauth_client: web::Data<OAuthClientType>, 871 + db_pool: web::Data<Arc<Pool>>, 872 + handle_resolver: web::Data<HandleResolver>, 873 + ) -> Result<impl Responder> { 874 + const TITLE: &str = "Home"; 875 + // Fetch data stored in our SQLite 876 + let mut statuses = StatusFromDb::load_latest_statuses(&db_pool) 877 + .await 878 + .unwrap_or_else(|err| { 879 + log::error!("Error loading statuses: {err}"); 880 + vec![] 881 + }); 882 + 883 + // We resolve the handles to the DID. This is a bit messy atm, 884 + // and there are hopes to find a cleaner way 885 + // to handle resolving the DIDs and formating the handles, 886 + // But it gets the job done for the purpose of this tutorial. 887 + // PRs are welcomed! 888 + 889 + //Simple way to cut down on resolve calls if we already know the handle for the did 890 + let mut quick_resolve_map: HashMap<Did, String> = HashMap::new(); 891 + for db_status in &mut statuses { 892 + let authors_did = Did::new(db_status.author_did.clone()).expect("failed to parse did"); 893 + //Check to see if we already resolved it to cut down on resolve requests 894 + match quick_resolve_map.get(&authors_did) { 895 + None => {} 896 + Some(found_handle) => { 897 + db_status.handle = Some(found_handle.clone()); 898 + continue; 899 + } 900 + } 901 + //Attempts to resolve the DID to a handle 902 + db_status.handle = match handle_resolver.resolve(&authors_did).await { 903 + Ok(did_doc) => { 904 + match did_doc.also_known_as { 905 + None => None, 906 + Some(also_known_as) => { 907 + match also_known_as.is_empty() { 908 + true => None, 909 + false => { 910 + //also_known as a list starts the array with the highest priority handle 911 + let formatted_handle = 912 + format!("@{}", also_known_as[0]).replace("at://", ""); 913 + quick_resolve_map.insert(authors_did, formatted_handle.clone()); 914 + Some(formatted_handle) 915 + } 916 + } 917 + } 918 + } 919 + } 920 + Err(err) => { 921 + log::error!("Error resolving did: {err}"); 922 + None 923 + } 924 + }; 925 + } 926 + // ... 927 + ``` 928 + >[!NOTE] 929 + > We use a newly released handle resolver from atrium. 930 + > Can see 931 + > how it is set up in [./src/main.rs](https://github.com/fatfingers23/rusty_statusphere_example_app/blob/a13ab7eb8fcba901a483468f7fd7c56b2948972d/src/main.rs#L508) 932 + 933 + 934 + Our HTML can now list these status records: 935 + 936 + ```html 937 + <!-- ./templates/home.html --> 938 + {% for status in statuses %} 939 + <div class="{% if loop.first %} status-line no-line {% else %} status-line {% endif %} "> 940 + <div> 941 + <div class="status">{{status.status}}</div> 942 + </div> 943 + <div class="desc"> 944 + <a class="author" 945 + href="https://bsky.app/profile/{{status.author_did}}">{{status.author_display_name()}}</a> 946 + {% if status.is_today() %} 947 + is feeling {{status.status}} today 948 + {% else %} 949 + was feeling {{status.status}} on {{status.created_at}} 950 + {% endif %} 951 + </div> 952 + </div> 953 + {% endfor %} 954 + ` 955 + })} 956 + ``` 957 + 958 + ![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) 959 + 960 + ## Step 8. Optimistic updates 961 + 962 + As a final optimization, let's introduce "optimistic updates." 963 + 964 + Remember the information flow loop with the repo write and the event log? 965 + 966 + !["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) 967 + 968 + Since we're updating our users' repos locally, we can short-circuit that flow to our own database: 969 + 970 + ![A diagram illustrating optimistic updates](https://atproto.com/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Fdiagram-optimistic-update.ca3f4cf1.png&w=1080&q=75) 971 + 972 + This is an important optimization to make, because it ensures that the user sees their own changes while using your app. 973 + When the event eventually arrives from the firehose, we just discard it since we already have it saved locally. 974 + 975 + To do this, we just update `POST /status` to include an additional write to our SQLite DB: 976 + 977 + ```rust 978 + /** ./src/main.rs **/ 979 + /// Creates a new status 980 + #[post("/status")] 981 + async fn status( 982 + request: HttpRequest, 983 + session: Session, 984 + oauth_client: web::Data<OAuthClientType>, 985 + db_pool: web::Data<Arc<Pool>>, 986 + form: web::Form<StatusForm>, 987 + ) -> HttpResponse { 988 + //... 989 + let create_result = agent 990 + .api 991 + .com 992 + .atproto 993 + .repo 994 + .create_record( 995 + atrium_api::com::atproto::repo::create_record::InputData { 996 + collection: Status::NSID.parse().unwrap(), 997 + repo: did.into(), 998 + rkey: None, 999 + record: status.into(), 1000 + swap_commit: None, 1001 + validate: None, 1002 + } 1003 + .into(), 1004 + ) 1005 + .await; 1006 + 1007 + match create_result { 1008 + Ok(record) => { 1009 + let status = StatusFromDb::new( 1010 + record.uri.clone(), 1011 + did_string, 1012 + form.status.clone(), 1013 + ); 1014 + 1015 + let _ = status.save(db_pool).await; 1016 + Redirect::to("/") 1017 + .see_other() 1018 + .respond_to(&request) 1019 + .map_into_boxed_body() 1020 + } 1021 + Err(err) => { 1022 + log::error!("Error creating status: {err}"); 1023 + let error_html = ErrorTemplate { 1024 + title: "Error", 1025 + error: "Was an error creating the status, please check the logs.", 1026 + } 1027 + .render() 1028 + .expect("template should be valid"); 1029 + HttpResponse::Ok().body(error_html) 1030 + } 1031 + } 1032 + //... 1033 + } 1034 + ``` 1035 + 1036 + You'll notice this code looks almost exactly like what we're doing in `ingester.rs`. 1037 + 1038 + ## Thinking in AT Proto 1039 + 1040 + In this tutorial we've covered the key steps to building an atproto app. Data is published in its canonical form on 1041 + users' `at://` repos and then aggregated into apps' databases to produce views of the network. 1042 + 1043 + When building your app, think in these four key steps: 1044 + 1045 + - Design the [Lexicon](#) schemas for the records you'll publish into the Atmosphere. 1046 + - Create a database for aggregating the records into useful views. 1047 + - Build your application to write the records on your users' repos. 1048 + - Listen to the firehose to aggregate data across the network. 1049 + 1050 + Remember this flow of information throughout: 1051 + 1052 + ![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) 1053 + 1054 + This is how every app in the Atmosphere works, including the [Bluesky social app](https://bsky.app). 1055 + 1056 + ## Next steps 1057 + 1058 + If you want to practice what you've learned, here are some additional challenges you could try: 1059 + 1060 + - Sync the profile records of all users so that you can show their display names instead of their handles. 1061 + - Count the number of each status used and display the total counts. 1062 + - Fetch the authed user's `app.bsky.graph.follow` follows and show statuses from them. 1063 + - Create a different kind of schema, like a way to post links to websites and rate them 1 through 4 stars. 1064 + 1065 + [Ready to learn more? Specs, guides, and SDKs can be found here.](https://atproto.com/) 1066 + 1067 + >[!NOTE] 1068 + > Thank you for checking out my version of the Statusphere example project! 1069 + > There are parts of this I feel can be improved on and made more efficient, 1070 + > but I think it does a good job for providing you with a starting point to start building Rust applications in the Atmosphere. 1071 + > See something you think could be done better? Then please submit a PR! 1072 + > [@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 - }
+407
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 = conn.prepare("SELECT * FROM status ORDER BY indexedAt DESC")?; 181 + let status_iter = stmt 182 + .query_map([], |row| Ok(Self::map_from_row(row).unwrap())) 183 + .unwrap(); 184 + 185 + let mut statuses = Vec::new(); 186 + for status in status_iter { 187 + statuses.push(status?); 188 + } 189 + Ok(statuses) 190 + }) 191 + .await?) 192 + } 193 + 194 + /// Loads the logged-in users current status 195 + pub async fn my_status( 196 + pool: &Data<Arc<Pool>>, 197 + did: &str, 198 + ) -> Result<Option<Self>, async_sqlite::Error> { 199 + let did = did.to_string(); 200 + pool.conn(move |conn| { 201 + let mut stmt = conn.prepare( 202 + "SELECT * FROM status WHERE authorDid = ?1 ORDER BY createdAt DESC LIMIT 1", 203 + )?; 204 + stmt.query_row([did.as_str()], |row| Self::map_from_row(row)) 205 + .map(Some) 206 + .or_else(|err| { 207 + if err == rusqlite::Error::QueryReturnedNoRows { 208 + Ok(None) 209 + } else { 210 + Err(err) 211 + } 212 + }) 213 + }) 214 + .await 215 + } 216 + 217 + /// ui helper to show a handle or did if the handle cannot be found 218 + pub fn author_display_name(&self) -> String { 219 + match self.handle.as_ref() { 220 + Some(handle) => handle.to_string(), 221 + None => self.author_did.to_string(), 222 + } 223 + } 224 + } 225 + 226 + /// AuthSession table data type 227 + #[derive(Debug, Clone, Deserialize, Serialize)] 228 + pub struct AuthSession { 229 + pub key: String, 230 + pub session: String, 231 + } 232 + 233 + impl AuthSession { 234 + /// Creates a new [AuthSession] 235 + pub fn new<V>(key: String, session: V) -> Self 236 + where 237 + V: Serialize, 238 + { 239 + let session = serde_json::to_string(&session).unwrap(); 240 + Self { 241 + key: key.to_string(), 242 + session, 243 + } 244 + } 245 + 246 + /// Helper to map from [Row] to [AuthSession] 247 + fn map_from_row(row: &Row) -> Result<Self, Error> { 248 + let key: String = row.get(0)?; 249 + let session: String = row.get(1)?; 250 + Ok(Self { key, session }) 251 + } 252 + 253 + /// Gets a session by the users did(key) 254 + pub async fn get_by_did(pool: &Pool, did: String) -> Result<Option<Self>, async_sqlite::Error> { 255 + let did = Did::new(did).unwrap(); 256 + pool.conn(move |conn| { 257 + let mut stmt = conn.prepare("SELECT * FROM auth_session WHERE key = ?1")?; 258 + stmt.query_row([did.as_str()], |row| Self::map_from_row(row)) 259 + .map(Some) 260 + .or_else(|err| { 261 + if err == Error::QueryReturnedNoRows { 262 + Ok(None) 263 + } else { 264 + Err(err) 265 + } 266 + }) 267 + }) 268 + .await 269 + } 270 + 271 + /// Saves or updates the session by its did(key) 272 + pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> { 273 + let cloned_self = self.clone(); 274 + pool.conn(move |conn| { 275 + //We check to see if the session already exists, if so we need to update not insert 276 + let mut stmt = conn.prepare("SELECT COUNT(*) FROM auth_session WHERE key = ?1")?; 277 + let count: i64 = stmt.query_row([&cloned_self.key], |row| row.get(0))?; 278 + match count > 0 { 279 + true => { 280 + let mut update_stmt = 281 + conn.prepare("UPDATE auth_session SET session = ?2 WHERE key = ?1")?; 282 + update_stmt.execute([&cloned_self.key, &cloned_self.session])?; 283 + Ok(()) 284 + } 285 + false => { 286 + conn.execute( 287 + "INSERT INTO auth_session (key, session) VALUES (?1, ?2)", 288 + [&cloned_self.key, &cloned_self.session], 289 + )?; 290 + Ok(()) 291 + } 292 + } 293 + }) 294 + .await?; 295 + Ok(()) 296 + } 297 + 298 + /// Deletes the session by did 299 + pub async fn delete_by_did(pool: &Pool, did: String) -> Result<(), async_sqlite::Error> { 300 + pool.conn(move |conn| { 301 + let mut stmt = conn.prepare("DELETE FROM auth_session WHERE key = ?1")?; 302 + stmt.execute([&did]) 303 + }) 304 + .await?; 305 + Ok(()) 306 + } 307 + 308 + /// Deletes all the sessions 309 + pub async fn delete_all(pool: &Pool) -> Result<(), async_sqlite::Error> { 310 + pool.conn(move |conn| { 311 + let mut stmt = conn.prepare("DELETE FROM auth_session")?; 312 + stmt.execute([]) 313 + }) 314 + .await?; 315 + Ok(()) 316 + } 317 + } 318 + 319 + /// AuthState table datatype 320 + #[derive(Debug, Clone, Deserialize, Serialize)] 321 + pub struct AuthState { 322 + pub key: String, 323 + pub state: String, 324 + } 325 + 326 + impl AuthState { 327 + /// Creates a new [AuthState] 328 + pub fn new<V>(key: String, state: V) -> Self 329 + where 330 + V: Serialize, 331 + { 332 + let state = serde_json::to_string(&state).unwrap(); 333 + Self { 334 + key: key.to_string(), 335 + state, 336 + } 337 + } 338 + 339 + /// Helper to map from [Row] to [AuthState] 340 + fn map_from_row(row: &Row) -> Result<Self, Error> { 341 + let key: String = row.get(0)?; 342 + let state: String = row.get(1)?; 343 + Ok(Self { key, state }) 344 + } 345 + 346 + /// Gets a state by the users key 347 + pub async fn get_by_key(pool: &Pool, key: String) -> Result<Option<Self>, async_sqlite::Error> { 348 + pool.conn(move |conn| { 349 + let mut stmt = conn.prepare("SELECT * FROM auth_state WHERE key = ?1")?; 350 + stmt.query_row([key.as_str()], |row| Self::map_from_row(row)) 351 + .map(Some) 352 + .or_else(|err| { 353 + if err == Error::QueryReturnedNoRows { 354 + Ok(None) 355 + } else { 356 + Err(err) 357 + } 358 + }) 359 + }) 360 + .await 361 + } 362 + 363 + /// Saves or updates the state by its key 364 + pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> { 365 + let cloned_self = self.clone(); 366 + pool.conn(move |conn| { 367 + //We check to see if the state already exists, if so we need to update 368 + let mut stmt = conn.prepare("SELECT COUNT(*) FROM auth_state WHERE key = ?1")?; 369 + let count: i64 = stmt.query_row([&cloned_self.key], |row| row.get(0))?; 370 + match count > 0 { 371 + true => { 372 + let mut update_stmt = 373 + conn.prepare("UPDATE auth_state SET state = ?2 WHERE key = ?1")?; 374 + update_stmt.execute([&cloned_self.key, &cloned_self.state])?; 375 + Ok(()) 376 + } 377 + false => { 378 + conn.execute( 379 + "INSERT INTO auth_state (key, state) VALUES (?1, ?2)", 380 + [&cloned_self.key, &cloned_self.state], 381 + )?; 382 + Ok(()) 383 + } 384 + } 385 + }) 386 + .await?; 387 + Ok(()) 388 + } 389 + 390 + pub async fn delete_by_key(pool: &Pool, key: String) -> Result<(), async_sqlite::Error> { 391 + pool.conn(move |conn| { 392 + let mut stmt = conn.prepare("DELETE FROM auth_state WHERE key = ?1")?; 393 + stmt.execute([&key]) 394 + }) 395 + .await?; 396 + Ok(()) 397 + } 398 + 399 + pub async fn delete_all(pool: &Pool) -> Result<(), async_sqlite::Error> { 400 + pool.conn(move |conn| { 401 + let mut stmt = conn.prepare("DELETE FROM auth_state")?; 402 + stmt.execute([]) 403 + }) 404 + .await?; 405 + Ok(()) 406 + } 407 + }
+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 + }
+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;
+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 + }
+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 + }
+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. My try relogging 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 users 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 users 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 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>