+7
.env.template
+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
+
+1061
-129
Cargo.lock
+1061
-129
Cargo.lock
···
44
44
45
45
[[package]]
46
46
name = "actix-http"
47
-
version = "3.9.0"
47
+
version = "3.10.0"
48
48
source = "registry+https://github.com/rust-lang/crates.io-index"
49
-
checksum = "d48f96fc3003717aeb9856ca3d02a8c7de502667ad76eeacd830b48d2e91fac4"
49
+
checksum = "0fa882656b67966045e4152c634051e70346939fced7117d5f0b52146a7c74c9"
50
50
dependencies = [
51
51
"actix-codec",
52
52
"actix-rt",
53
53
"actix-service",
54
54
"actix-utils",
55
-
"ahash",
56
55
"base64 0.22.1",
57
56
"bitflags",
58
57
"brotli",
59
58
"bytes",
60
59
"bytestring",
61
-
"derive_more 0.99.19",
60
+
"derive_more 2.0.1",
62
61
"encoding_rs",
63
62
"flate2",
63
+
"foldhash",
64
64
"futures-core",
65
65
"h2",
66
66
"http 0.2.12",
···
72
72
"mime",
73
73
"percent-encoding",
74
74
"pin-project-lite",
75
-
"rand",
75
+
"rand 0.9.0",
76
76
"sha1",
77
77
"smallvec",
78
78
"tokio",
···
155
155
"actix-web",
156
156
"anyhow",
157
157
"derive_more 1.0.0",
158
-
"rand",
158
+
"rand 0.8.5",
159
159
"serde",
160
160
"serde_json",
161
161
"tracing",
···
173
173
174
174
[[package]]
175
175
name = "actix-web"
176
-
version = "4.9.0"
176
+
version = "4.10.2"
177
177
source = "registry+https://github.com/rust-lang/crates.io-index"
178
-
checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38"
178
+
checksum = "f2e3b15b3dc6c6ed996e4032389e9849d4ab002b1e92fbfe85b5f307d1479b4d"
179
179
dependencies = [
180
180
"actix-codec",
181
181
"actix-http",
···
186
186
"actix-service",
187
187
"actix-utils",
188
188
"actix-web-codegen",
189
-
"ahash",
190
189
"bytes",
191
190
"bytestring",
192
191
"cfg-if",
193
192
"cookie",
194
-
"derive_more 0.99.19",
193
+
"derive_more 2.0.1",
195
194
"encoding_rs",
195
+
"foldhash",
196
196
"futures-core",
197
197
"futures-util",
198
198
"impl-more",
···
210
210
"smallvec",
211
211
"socket2",
212
212
"time",
213
+
"tracing",
213
214
"url",
214
215
]
215
216
···
282
283
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
283
284
dependencies = [
284
285
"cfg-if",
285
-
"getrandom 0.2.15",
286
286
"once_cell",
287
287
"version_check",
288
-
"zerocopy",
288
+
"zerocopy 0.7.35",
289
289
]
290
290
291
291
[[package]]
···
391
391
392
392
[[package]]
393
393
name = "askama"
394
-
version = "0.12.1"
394
+
version = "0.13.0"
395
395
source = "registry+https://github.com/rust-lang/crates.io-index"
396
-
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
396
+
checksum = "9a4e46abb203e00ef226442d452769233142bbfdd79c3941e84c8e61c4112543"
397
397
dependencies = [
398
398
"askama_derive",
399
-
"askama_escape",
400
-
"humansize",
401
-
"num-traits",
399
+
"itoa",
402
400
"percent-encoding",
401
+
"serde",
402
+
"serde_json",
403
403
]
404
404
405
405
[[package]]
406
406
name = "askama_derive"
407
-
version = "0.12.5"
407
+
version = "0.13.0"
408
408
source = "registry+https://github.com/rust-lang/crates.io-index"
409
-
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
409
+
checksum = "54398906821fd32c728135f7b351f0c7494ab95ae421d41b6f5a020e158f28a6"
410
410
dependencies = [
411
411
"askama_parser",
412
412
"basic-toml",
413
-
"mime",
414
-
"mime_guess",
413
+
"memchr",
415
414
"proc-macro2",
416
415
"quote",
416
+
"rustc-hash",
417
417
"serde",
418
+
"serde_derive",
418
419
"syn",
419
420
]
420
421
421
422
[[package]]
422
-
name = "askama_escape"
423
-
version = "0.10.3"
424
-
source = "registry+https://github.com/rust-lang/crates.io-index"
425
-
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
426
-
427
-
[[package]]
428
423
name = "askama_parser"
429
-
version = "0.2.1"
424
+
version = "0.13.0"
430
425
source = "registry+https://github.com/rust-lang/crates.io-index"
431
-
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
426
+
checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
432
427
dependencies = [
433
-
"nom",
428
+
"memchr",
429
+
"serde",
430
+
"serde_derive",
431
+
"winnow",
434
432
]
435
433
436
434
[[package]]
···
458
456
]
459
457
460
458
[[package]]
461
-
name = "async-stream"
462
-
version = "0.3.6"
459
+
name = "async-sqlite"
460
+
version = "0.5.0"
463
461
source = "registry+https://github.com/rust-lang/crates.io-index"
464
-
checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
462
+
checksum = "60659f08ccb3a20c15af150ae736cde366fa0657246be9d194affb0149be188f"
465
463
dependencies = [
466
-
"async-stream-impl",
467
-
"futures-core",
468
-
"pin-project-lite",
464
+
"crossbeam-channel",
465
+
"futures-channel",
466
+
"futures-util",
467
+
"rusqlite",
469
468
]
470
469
471
470
[[package]]
472
-
name = "async-stream-impl"
473
-
version = "0.3.6"
471
+
name = "async-trait"
472
+
version = "0.1.88"
474
473
source = "registry+https://github.com/rust-lang/crates.io-index"
475
-
checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
474
+
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
476
475
dependencies = [
477
476
"proc-macro2",
478
477
"quote",
···
481
480
482
481
[[package]]
483
482
name = "atrium-api"
484
-
version = "0.25.0"
483
+
version = "0.25.2"
485
484
source = "registry+https://github.com/rust-lang/crates.io-index"
486
-
checksum = "ea3ea578c768ec91082e424a8d139517b2cb5c75149bf3cec04371a1e74f00f2"
485
+
checksum = "0d4eb9b4787aba546015c8ccda1d3924c157cee13d67848997fba74ac8144a07"
487
486
dependencies = [
488
487
"atrium-common",
489
488
"atrium-xrpc",
···
502
501
503
502
[[package]]
504
503
name = "atrium-common"
505
-
version = "0.1.0"
504
+
version = "0.1.1"
506
505
source = "registry+https://github.com/rust-lang/crates.io-index"
507
-
checksum = "168e558408847bfed69df1033a32fd051f7a037ebc90ea46e588ccb2bfbd7233"
506
+
checksum = "ba30d2f9e1a8b3db8fc97d0a5f91ee5a28f8acdddb771ad74c1b08eda357ca3d"
508
507
dependencies = [
509
508
"dashmap",
510
509
"lru",
···
516
515
]
517
516
518
517
[[package]]
519
-
name = "atrium-xrpc"
520
-
version = "0.12.1"
518
+
name = "atrium-identity"
519
+
version = "0.1.3"
521
520
source = "registry+https://github.com/rust-lang/crates.io-index"
522
-
checksum = "6b4956d94147cfbb669c68f654eb4fd6a1d00648c810cec79d04ec5425b8f378"
521
+
checksum = "007c7fdb0e026c7d01697b78263b2d85742b5113fbc5263f8885280cacceca05"
523
522
dependencies = [
524
-
"http 1.2.0",
523
+
"atrium-api",
524
+
"atrium-common",
525
+
"atrium-xrpc",
525
526
"serde",
526
527
"serde_html_form",
527
528
"serde_json",
···
530
531
]
531
532
532
533
[[package]]
533
-
name = "atrium-xrpc-client"
534
-
version = "0.5.11"
534
+
name = "atrium-oauth"
535
+
version = "0.1.1"
535
536
source = "registry+https://github.com/rust-lang/crates.io-index"
536
-
checksum = "9bab4287ccef501b3892e1325280e61ae79a96eb9ee63dceabc0ed3bea35f2eb"
537
+
checksum = "24e59e30ae1aa9bbb99ebf2fa5ca40a8ca6665b6b7e4d1de322d99544045e91e"
537
538
dependencies = [
539
+
"atrium-api",
540
+
"atrium-common",
541
+
"atrium-identity",
538
542
"atrium-xrpc",
543
+
"base64 0.22.1",
544
+
"chrono",
545
+
"dashmap",
546
+
"ecdsa",
547
+
"elliptic-curve",
548
+
"jose-jwa",
549
+
"jose-jwk",
550
+
"p256",
551
+
"rand 0.8.5",
539
552
"reqwest",
553
+
"serde",
554
+
"serde_html_form",
555
+
"serde_json",
556
+
"sha2",
557
+
"thiserror",
558
+
"tokio",
559
+
"trait-variant",
560
+
]
561
+
562
+
[[package]]
563
+
name = "atrium-xrpc"
564
+
version = "0.12.2"
565
+
source = "registry+https://github.com/rust-lang/crates.io-index"
566
+
checksum = "18a9e526cb2ed3e0a2ca78c3ce2a943d9041a68e067dadf42923b523771e07df"
567
+
dependencies = [
568
+
"http 1.2.0",
569
+
"serde",
570
+
"serde_html_form",
571
+
"serde_json",
572
+
"thiserror",
573
+
"trait-variant",
540
574
]
541
575
542
576
[[package]]
···
557
591
"miniz_oxide",
558
592
"object",
559
593
"rustc-demangle",
560
-
"windows-targets",
594
+
"windows-targets 0.52.6",
561
595
]
562
596
563
597
[[package]]
···
567
601
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
568
602
569
603
[[package]]
604
+
name = "base16ct"
605
+
version = "0.2.0"
606
+
source = "registry+https://github.com/rust-lang/crates.io-index"
607
+
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
608
+
609
+
[[package]]
570
610
name = "base64"
571
611
version = "0.20.0"
572
612
source = "registry+https://github.com/rust-lang/crates.io-index"
···
574
614
575
615
[[package]]
576
616
name = "base64"
617
+
version = "0.21.7"
618
+
source = "registry+https://github.com/rust-lang/crates.io-index"
619
+
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
620
+
621
+
[[package]]
622
+
name = "base64"
577
623
version = "0.22.1"
578
624
source = "registry+https://github.com/rust-lang/crates.io-index"
579
625
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
580
626
581
627
[[package]]
628
+
name = "base64ct"
629
+
version = "1.7.3"
630
+
source = "registry+https://github.com/rust-lang/crates.io-index"
631
+
checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3"
632
+
633
+
[[package]]
582
634
name = "basic-toml"
583
635
version = "0.1.10"
584
636
source = "registry+https://github.com/rust-lang/crates.io-index"
···
603
655
]
604
656
605
657
[[package]]
658
+
name = "bon"
659
+
version = "3.5.1"
660
+
source = "registry+https://github.com/rust-lang/crates.io-index"
661
+
checksum = "65268237be94042665b92034f979c42d431d2fd998b49809543afe3e66abad1c"
662
+
dependencies = [
663
+
"bon-macros",
664
+
"rustversion",
665
+
]
666
+
667
+
[[package]]
668
+
name = "bon-macros"
669
+
version = "3.5.1"
670
+
source = "registry+https://github.com/rust-lang/crates.io-index"
671
+
checksum = "803c95b2ecf650eb10b5f87dda6b9f6a1b758cee53245e2b7b825c9b3803a443"
672
+
dependencies = [
673
+
"darling",
674
+
"ident_case",
675
+
"prettyplease",
676
+
"proc-macro2",
677
+
"quote",
678
+
"rustversion",
679
+
"syn",
680
+
]
681
+
682
+
[[package]]
606
683
name = "brotli"
607
-
version = "6.0.0"
684
+
version = "7.0.0"
608
685
source = "registry+https://github.com/rust-lang/crates.io-index"
609
-
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
686
+
checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd"
610
687
dependencies = [
611
688
"alloc-no-stdlib",
612
689
"alloc-stdlib",
···
722
799
]
723
800
724
801
[[package]]
802
+
name = "const-oid"
803
+
version = "0.9.6"
804
+
source = "registry+https://github.com/rust-lang/crates.io-index"
805
+
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
806
+
807
+
[[package]]
725
808
name = "convert_case"
726
809
version = "0.4.0"
727
810
source = "registry+https://github.com/rust-lang/crates.io-index"
···
738
821
"hkdf",
739
822
"hmac",
740
823
"percent-encoding",
741
-
"rand",
824
+
"rand 0.8.5",
742
825
"sha2",
743
826
"subtle",
744
827
"time",
···
813
896
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
814
897
815
898
[[package]]
899
+
name = "crypto-bigint"
900
+
version = "0.5.5"
901
+
source = "registry+https://github.com/rust-lang/crates.io-index"
902
+
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
903
+
dependencies = [
904
+
"generic-array",
905
+
"rand_core 0.6.4",
906
+
"subtle",
907
+
"zeroize",
908
+
]
909
+
910
+
[[package]]
816
911
name = "crypto-common"
817
912
version = "0.1.6"
818
913
source = "registry+https://github.com/rust-lang/crates.io-index"
819
914
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
820
915
dependencies = [
821
916
"generic-array",
822
-
"rand_core",
917
+
"rand_core 0.6.4",
823
918
"typenum",
824
919
]
825
920
···
833
928
]
834
929
835
930
[[package]]
931
+
name = "darling"
932
+
version = "0.20.11"
933
+
source = "registry+https://github.com/rust-lang/crates.io-index"
934
+
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
935
+
dependencies = [
936
+
"darling_core",
937
+
"darling_macro",
938
+
]
939
+
940
+
[[package]]
941
+
name = "darling_core"
942
+
version = "0.20.11"
943
+
source = "registry+https://github.com/rust-lang/crates.io-index"
944
+
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
945
+
dependencies = [
946
+
"fnv",
947
+
"ident_case",
948
+
"proc-macro2",
949
+
"quote",
950
+
"strsim",
951
+
"syn",
952
+
]
953
+
954
+
[[package]]
955
+
name = "darling_macro"
956
+
version = "0.20.11"
957
+
source = "registry+https://github.com/rust-lang/crates.io-index"
958
+
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
959
+
dependencies = [
960
+
"darling_core",
961
+
"quote",
962
+
"syn",
963
+
]
964
+
965
+
[[package]]
836
966
name = "dashmap"
837
967
version = "6.1.0"
838
968
source = "registry+https://github.com/rust-lang/crates.io-index"
···
873
1003
]
874
1004
875
1005
[[package]]
1006
+
name = "der"
1007
+
version = "0.7.9"
1008
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1009
+
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
1010
+
dependencies = [
1011
+
"const-oid",
1012
+
"zeroize",
1013
+
]
1014
+
1015
+
[[package]]
876
1016
name = "deranged"
877
1017
version = "0.3.11"
878
1018
source = "registry+https://github.com/rust-lang/crates.io-index"
···
882
1022
]
883
1023
884
1024
[[package]]
1025
+
name = "derive_builder"
1026
+
version = "0.20.2"
1027
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1028
+
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
1029
+
dependencies = [
1030
+
"derive_builder_macro",
1031
+
]
1032
+
1033
+
[[package]]
1034
+
name = "derive_builder_core"
1035
+
version = "0.20.2"
1036
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1037
+
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
1038
+
dependencies = [
1039
+
"darling",
1040
+
"proc-macro2",
1041
+
"quote",
1042
+
"syn",
1043
+
]
1044
+
1045
+
[[package]]
1046
+
name = "derive_builder_macro"
1047
+
version = "0.20.2"
1048
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1049
+
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
1050
+
dependencies = [
1051
+
"derive_builder_core",
1052
+
"syn",
1053
+
]
1054
+
1055
+
[[package]]
885
1056
name = "derive_more"
886
1057
version = "0.99.19"
887
1058
source = "registry+https://github.com/rust-lang/crates.io-index"
···
900
1071
source = "registry+https://github.com/rust-lang/crates.io-index"
901
1072
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
902
1073
dependencies = [
903
-
"derive_more-impl",
1074
+
"derive_more-impl 1.0.0",
1075
+
]
1076
+
1077
+
[[package]]
1078
+
name = "derive_more"
1079
+
version = "2.0.1"
1080
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1081
+
checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
1082
+
dependencies = [
1083
+
"derive_more-impl 2.0.1",
904
1084
]
905
1085
906
1086
[[package]]
···
916
1096
]
917
1097
918
1098
[[package]]
1099
+
name = "derive_more-impl"
1100
+
version = "2.0.1"
1101
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1102
+
checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
1103
+
dependencies = [
1104
+
"proc-macro2",
1105
+
"quote",
1106
+
"syn",
1107
+
"unicode-xid",
1108
+
]
1109
+
1110
+
[[package]]
919
1111
name = "digest"
920
1112
version = "0.10.7"
921
1113
source = "registry+https://github.com/rust-lang/crates.io-index"
922
1114
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
923
1115
dependencies = [
924
1116
"block-buffer",
1117
+
"const-oid",
925
1118
"crypto-common",
926
1119
"subtle",
927
1120
]
···
938
1131
]
939
1132
940
1133
[[package]]
1134
+
name = "dotenv"
1135
+
version = "0.15.0"
1136
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1137
+
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
1138
+
1139
+
[[package]]
1140
+
name = "ecdsa"
1141
+
version = "0.16.9"
1142
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1143
+
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
1144
+
dependencies = [
1145
+
"der",
1146
+
"digest",
1147
+
"elliptic-curve",
1148
+
"rfc6979",
1149
+
"signature",
1150
+
]
1151
+
1152
+
[[package]]
1153
+
name = "elliptic-curve"
1154
+
version = "0.13.8"
1155
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1156
+
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
1157
+
dependencies = [
1158
+
"base16ct",
1159
+
"crypto-bigint",
1160
+
"digest",
1161
+
"ff",
1162
+
"generic-array",
1163
+
"group",
1164
+
"rand_core 0.6.4",
1165
+
"sec1",
1166
+
"subtle",
1167
+
"zeroize",
1168
+
]
1169
+
1170
+
[[package]]
941
1171
name = "encoding_rs"
942
1172
version = "0.8.35"
943
1173
source = "registry+https://github.com/rust-lang/crates.io-index"
···
947
1177
]
948
1178
949
1179
[[package]]
1180
+
name = "enum-as-inner"
1181
+
version = "0.6.1"
1182
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1183
+
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
1184
+
dependencies = [
1185
+
"heck",
1186
+
"proc-macro2",
1187
+
"quote",
1188
+
"syn",
1189
+
]
1190
+
1191
+
[[package]]
950
1192
name = "env_filter"
951
1193
version = "0.1.3"
952
1194
source = "registry+https://github.com/rust-lang/crates.io-index"
···
958
1200
959
1201
[[package]]
960
1202
name = "env_logger"
961
-
version = "0.11.6"
1203
+
version = "0.11.7"
962
1204
source = "registry+https://github.com/rust-lang/crates.io-index"
963
-
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
1205
+
checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
964
1206
dependencies = [
965
1207
"anstream",
966
1208
"anstyle",
967
1209
"env_filter",
968
-
"humantime",
1210
+
"jiff",
969
1211
"log",
970
1212
]
971
1213
···
1007
1249
]
1008
1250
1009
1251
[[package]]
1252
+
name = "fallible-iterator"
1253
+
version = "0.3.0"
1254
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1255
+
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
1256
+
1257
+
[[package]]
1258
+
name = "fallible-streaming-iterator"
1259
+
version = "0.1.9"
1260
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1261
+
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
1262
+
1263
+
[[package]]
1010
1264
name = "fastrand"
1011
1265
version = "2.3.0"
1012
1266
source = "registry+https://github.com/rust-lang/crates.io-index"
1013
1267
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
1014
1268
1015
1269
[[package]]
1270
+
name = "ff"
1271
+
version = "0.13.1"
1272
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1273
+
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
1274
+
dependencies = [
1275
+
"rand_core 0.6.4",
1276
+
"subtle",
1277
+
]
1278
+
1279
+
[[package]]
1016
1280
name = "flate2"
1017
1281
version = "1.1.0"
1018
1282
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1023
1287
]
1024
1288
1025
1289
[[package]]
1290
+
name = "flume"
1291
+
version = "0.11.1"
1292
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1293
+
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
1294
+
dependencies = [
1295
+
"futures-core",
1296
+
"futures-sink",
1297
+
"nanorand",
1298
+
"spin",
1299
+
]
1300
+
1301
+
[[package]]
1026
1302
name = "fnv"
1027
1303
version = "1.0.7"
1028
1304
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1074
1350
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
1075
1351
1076
1352
[[package]]
1353
+
name = "futures-io"
1354
+
version = "0.3.31"
1355
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1356
+
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
1357
+
1358
+
[[package]]
1077
1359
name = "futures-macro"
1078
1360
version = "0.3.31"
1079
1361
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1104
1386
dependencies = [
1105
1387
"futures-core",
1106
1388
"futures-macro",
1389
+
"futures-sink",
1107
1390
"futures-task",
1108
1391
"pin-project-lite",
1109
1392
"pin-utils",
···
1120
1403
"libc",
1121
1404
"log",
1122
1405
"rustversion",
1123
-
"windows",
1406
+
"windows 0.58.0",
1124
1407
]
1125
1408
1126
1409
[[package]]
···
1131
1414
dependencies = [
1132
1415
"typenum",
1133
1416
"version_check",
1417
+
"zeroize",
1134
1418
]
1135
1419
1136
1420
[[package]]
···
1140
1424
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
1141
1425
dependencies = [
1142
1426
"cfg-if",
1427
+
"js-sys",
1143
1428
"libc",
1144
1429
"wasi 0.11.0+wasi-snapshot-preview1",
1430
+
"wasm-bindgen",
1145
1431
]
1146
1432
1147
1433
[[package]]
···
1153
1439
"cfg-if",
1154
1440
"libc",
1155
1441
"wasi 0.13.3+wasi-0.2.2",
1156
-
"windows-targets",
1442
+
"windows-targets 0.52.6",
1157
1443
]
1158
1444
1159
1445
[[package]]
···
1173
1459
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
1174
1460
1175
1461
[[package]]
1462
+
name = "group"
1463
+
version = "0.13.0"
1464
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1465
+
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
1466
+
dependencies = [
1467
+
"ff",
1468
+
"rand_core 0.6.4",
1469
+
"subtle",
1470
+
]
1471
+
1472
+
[[package]]
1176
1473
name = "h2"
1177
1474
version = "0.3.26"
1178
1475
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1209
1506
]
1210
1507
1211
1508
[[package]]
1509
+
name = "hashlink"
1510
+
version = "0.10.0"
1511
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1512
+
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
1513
+
dependencies = [
1514
+
"hashbrown 0.15.2",
1515
+
]
1516
+
1517
+
[[package]]
1518
+
name = "heck"
1519
+
version = "0.5.0"
1520
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1521
+
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
1522
+
1523
+
[[package]]
1524
+
name = "hickory-proto"
1525
+
version = "0.24.4"
1526
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1527
+
checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
1528
+
dependencies = [
1529
+
"async-trait",
1530
+
"cfg-if",
1531
+
"data-encoding",
1532
+
"enum-as-inner",
1533
+
"futures-channel",
1534
+
"futures-io",
1535
+
"futures-util",
1536
+
"idna",
1537
+
"ipnet",
1538
+
"once_cell",
1539
+
"rand 0.8.5",
1540
+
"thiserror",
1541
+
"tinyvec",
1542
+
"tokio",
1543
+
"tracing",
1544
+
"url",
1545
+
]
1546
+
1547
+
[[package]]
1548
+
name = "hickory-resolver"
1549
+
version = "0.24.4"
1550
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1551
+
checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
1552
+
dependencies = [
1553
+
"cfg-if",
1554
+
"futures-util",
1555
+
"hickory-proto",
1556
+
"ipconfig",
1557
+
"lru-cache",
1558
+
"once_cell",
1559
+
"parking_lot",
1560
+
"rand 0.8.5",
1561
+
"resolv-conf",
1562
+
"smallvec",
1563
+
"thiserror",
1564
+
"tokio",
1565
+
"tracing",
1566
+
]
1567
+
1568
+
[[package]]
1212
1569
name = "hkdf"
1213
1570
version = "0.12.4"
1214
1571
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1224
1581
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
1225
1582
dependencies = [
1226
1583
"digest",
1584
+
]
1585
+
1586
+
[[package]]
1587
+
name = "hostname"
1588
+
version = "0.4.0"
1589
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1590
+
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
1591
+
dependencies = [
1592
+
"cfg-if",
1593
+
"libc",
1594
+
"windows 0.52.0",
1227
1595
]
1228
1596
1229
1597
[[package]]
···
1288
1656
version = "1.0.3"
1289
1657
source = "registry+https://github.com/rust-lang/crates.io-index"
1290
1658
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
1291
-
1292
-
[[package]]
1293
-
name = "humansize"
1294
-
version = "2.1.3"
1295
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1296
-
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
1297
-
dependencies = [
1298
-
"libm",
1299
-
]
1300
-
1301
-
[[package]]
1302
-
name = "humantime"
1303
-
version = "2.1.0"
1304
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1305
-
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
1306
1659
1307
1660
[[package]]
1308
1661
name = "hyper"
···
1500
1853
]
1501
1854
1502
1855
[[package]]
1856
+
name = "ident_case"
1857
+
version = "1.0.1"
1858
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1859
+
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
1860
+
1861
+
[[package]]
1503
1862
name = "idna"
1504
1863
version = "1.0.3"
1505
1864
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1546
1905
]
1547
1906
1548
1907
[[package]]
1908
+
name = "ipconfig"
1909
+
version = "0.3.2"
1910
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1911
+
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
1912
+
dependencies = [
1913
+
"socket2",
1914
+
"widestring",
1915
+
"windows-sys 0.48.0",
1916
+
"winreg",
1917
+
]
1918
+
1919
+
[[package]]
1549
1920
name = "ipld-core"
1550
1921
version = "0.4.2"
1551
1922
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1575
1946
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1576
1947
1577
1948
[[package]]
1949
+
name = "jiff"
1950
+
version = "0.2.5"
1951
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1952
+
checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260"
1953
+
dependencies = [
1954
+
"jiff-static",
1955
+
"log",
1956
+
"portable-atomic",
1957
+
"portable-atomic-util",
1958
+
"serde",
1959
+
]
1960
+
1961
+
[[package]]
1962
+
name = "jiff-static"
1963
+
version = "0.2.5"
1964
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1965
+
checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c"
1966
+
dependencies = [
1967
+
"proc-macro2",
1968
+
"quote",
1969
+
"syn",
1970
+
]
1971
+
1972
+
[[package]]
1578
1973
name = "jobserver"
1579
1974
version = "0.1.32"
1580
1975
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1584
1979
]
1585
1980
1586
1981
[[package]]
1982
+
name = "jose-b64"
1983
+
version = "0.1.2"
1984
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1985
+
checksum = "bec69375368709666b21c76965ce67549f2d2db7605f1f8707d17c9656801b56"
1986
+
dependencies = [
1987
+
"base64ct",
1988
+
"serde",
1989
+
"subtle",
1990
+
"zeroize",
1991
+
]
1992
+
1993
+
[[package]]
1994
+
name = "jose-jwa"
1995
+
version = "0.1.2"
1996
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1997
+
checksum = "9ab78e053fe886a351d67cf0d194c000f9d0dcb92906eb34d853d7e758a4b3a7"
1998
+
dependencies = [
1999
+
"serde",
2000
+
]
2001
+
2002
+
[[package]]
2003
+
name = "jose-jwk"
2004
+
version = "0.1.2"
2005
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2006
+
checksum = "280fa263807fe0782ecb6f2baadc28dffc04e00558a58e33bfdb801d11fd58e7"
2007
+
dependencies = [
2008
+
"jose-b64",
2009
+
"jose-jwa",
2010
+
"p256",
2011
+
"serde",
2012
+
"zeroize",
2013
+
]
2014
+
2015
+
[[package]]
1587
2016
name = "js-sys"
1588
2017
version = "0.3.77"
1589
2018
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1621
2050
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
1622
2051
1623
2052
[[package]]
1624
-
name = "libm"
1625
-
version = "0.2.11"
2053
+
name = "libsqlite3-sys"
2054
+
version = "0.31.0"
1626
2055
source = "registry+https://github.com/rust-lang/crates.io-index"
1627
-
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
2056
+
checksum = "ad8935b44e7c13394a179a438e0cebba0fe08fe01b54f152e29a93b5cf993fd4"
2057
+
dependencies = [
2058
+
"cc",
2059
+
"pkg-config",
2060
+
"vcpkg",
2061
+
]
2062
+
2063
+
[[package]]
2064
+
name = "linked-hash-map"
2065
+
version = "0.5.6"
2066
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2067
+
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
1628
2068
1629
2069
[[package]]
1630
2070
name = "linux-raw-sys"
···
1667
2107
1668
2108
[[package]]
1669
2109
name = "log"
1670
-
version = "0.4.26"
2110
+
version = "0.4.27"
1671
2111
source = "registry+https://github.com/rust-lang/crates.io-index"
1672
-
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
2112
+
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
1673
2113
1674
2114
[[package]]
1675
2115
name = "loom"
···
1694
2134
]
1695
2135
1696
2136
[[package]]
2137
+
name = "lru-cache"
2138
+
version = "0.1.2"
2139
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2140
+
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
2141
+
dependencies = [
2142
+
"linked-hash-map",
2143
+
]
2144
+
2145
+
[[package]]
1697
2146
name = "matchers"
1698
2147
version = "0.1.0"
1699
2148
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1709
2158
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
1710
2159
1711
2160
[[package]]
2161
+
name = "metrics"
2162
+
version = "0.24.1"
2163
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2164
+
checksum = "7a7deb012b3b2767169ff203fadb4c6b0b82b947512e5eb9e0b78c2e186ad9e3"
2165
+
dependencies = [
2166
+
"ahash",
2167
+
"portable-atomic",
2168
+
]
2169
+
2170
+
[[package]]
1712
2171
name = "mime"
1713
2172
version = "0.3.17"
1714
2173
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1723
2182
"mime",
1724
2183
"unicase",
1725
2184
]
1726
-
1727
-
[[package]]
1728
-
name = "minimal-lexical"
1729
-
version = "0.2.1"
1730
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1731
-
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1732
2185
1733
2186
[[package]]
1734
2187
name = "miniz_oxide"
···
1796
2249
]
1797
2250
1798
2251
[[package]]
2252
+
name = "nanorand"
2253
+
version = "0.7.0"
2254
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2255
+
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
2256
+
dependencies = [
2257
+
"getrandom 0.2.15",
2258
+
]
2259
+
2260
+
[[package]]
1799
2261
name = "native-tls"
1800
2262
version = "0.2.14"
1801
2263
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1810
2272
"security-framework",
1811
2273
"security-framework-sys",
1812
2274
"tempfile",
1813
-
]
1814
-
1815
-
[[package]]
1816
-
name = "nom"
1817
-
version = "7.1.3"
1818
-
source = "registry+https://github.com/rust-lang/crates.io-index"
1819
-
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1820
-
dependencies = [
1821
-
"memchr",
1822
-
"minimal-lexical",
1823
2275
]
1824
2276
1825
2277
[[package]]
···
1919
2371
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
1920
2372
1921
2373
[[package]]
2374
+
name = "p256"
2375
+
version = "0.13.2"
2376
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2377
+
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
2378
+
dependencies = [
2379
+
"ecdsa",
2380
+
"elliptic-curve",
2381
+
"primeorder",
2382
+
"sha2",
2383
+
]
2384
+
2385
+
[[package]]
1922
2386
name = "parking"
1923
2387
version = "2.2.1"
1924
2388
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1944
2408
"libc",
1945
2409
"redox_syscall",
1946
2410
"smallvec",
1947
-
"windows-targets",
2411
+
"windows-targets 0.52.6",
1948
2412
]
1949
2413
1950
2414
[[package]]
···
1996
2460
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
1997
2461
1998
2462
[[package]]
2463
+
name = "portable-atomic-util"
2464
+
version = "0.2.4"
2465
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2466
+
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
2467
+
dependencies = [
2468
+
"portable-atomic",
2469
+
]
2470
+
2471
+
[[package]]
1999
2472
name = "powerfmt"
2000
2473
version = "0.2.0"
2001
2474
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2007
2480
source = "registry+https://github.com/rust-lang/crates.io-index"
2008
2481
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
2009
2482
dependencies = [
2010
-
"zerocopy",
2483
+
"zerocopy 0.7.35",
2484
+
]
2485
+
2486
+
[[package]]
2487
+
name = "prettyplease"
2488
+
version = "0.2.31"
2489
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2490
+
checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb"
2491
+
dependencies = [
2492
+
"proc-macro2",
2493
+
"syn",
2494
+
]
2495
+
2496
+
[[package]]
2497
+
name = "primeorder"
2498
+
version = "0.13.6"
2499
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2500
+
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
2501
+
dependencies = [
2502
+
"elliptic-curve",
2011
2503
]
2012
2504
2013
2505
[[package]]
···
2035
2527
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
2036
2528
dependencies = [
2037
2529
"libc",
2038
-
"rand_chacha",
2039
-
"rand_core",
2530
+
"rand_chacha 0.3.1",
2531
+
"rand_core 0.6.4",
2532
+
]
2533
+
2534
+
[[package]]
2535
+
name = "rand"
2536
+
version = "0.9.0"
2537
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2538
+
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
2539
+
dependencies = [
2540
+
"rand_chacha 0.9.0",
2541
+
"rand_core 0.9.3",
2542
+
"zerocopy 0.8.24",
2040
2543
]
2041
2544
2042
2545
[[package]]
···
2046
2549
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
2047
2550
dependencies = [
2048
2551
"ppv-lite86",
2049
-
"rand_core",
2552
+
"rand_core 0.6.4",
2553
+
]
2554
+
2555
+
[[package]]
2556
+
name = "rand_chacha"
2557
+
version = "0.9.0"
2558
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2559
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
2560
+
dependencies = [
2561
+
"ppv-lite86",
2562
+
"rand_core 0.9.3",
2050
2563
]
2051
2564
2052
2565
[[package]]
···
2056
2569
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
2057
2570
dependencies = [
2058
2571
"getrandom 0.2.15",
2572
+
]
2573
+
2574
+
[[package]]
2575
+
name = "rand_core"
2576
+
version = "0.9.3"
2577
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2578
+
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
2579
+
dependencies = [
2580
+
"getrandom 0.3.1",
2059
2581
]
2060
2582
2061
2583
[[package]]
···
2142
2664
"once_cell",
2143
2665
"percent-encoding",
2144
2666
"pin-project-lite",
2145
-
"rustls-pemfile",
2667
+
"rustls-pemfile 2.2.0",
2146
2668
"serde",
2147
2669
"serde_json",
2148
2670
"serde_urlencoded",
···
2160
2682
]
2161
2683
2162
2684
[[package]]
2685
+
name = "resolv-conf"
2686
+
version = "0.7.1"
2687
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2688
+
checksum = "48375394603e3dd4b2d64371f7148fd8c7baa2680e28741f2cb8d23b59e3d4c4"
2689
+
dependencies = [
2690
+
"hostname",
2691
+
]
2692
+
2693
+
[[package]]
2694
+
name = "rfc6979"
2695
+
version = "0.4.0"
2696
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2697
+
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
2698
+
dependencies = [
2699
+
"hmac",
2700
+
"subtle",
2701
+
]
2702
+
2703
+
[[package]]
2704
+
name = "ring"
2705
+
version = "0.17.14"
2706
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2707
+
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
2708
+
dependencies = [
2709
+
"cc",
2710
+
"cfg-if",
2711
+
"getrandom 0.2.15",
2712
+
"libc",
2713
+
"untrusted",
2714
+
"windows-sys 0.52.0",
2715
+
]
2716
+
2717
+
[[package]]
2718
+
name = "rocketman"
2719
+
version = "0.2.0"
2720
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2721
+
checksum = "4a3aae946adbfdcf80cad8793e02d8eb94be06c925331aa56aeb446795893361"
2722
+
dependencies = [
2723
+
"anyhow",
2724
+
"async-trait",
2725
+
"bon",
2726
+
"derive_builder",
2727
+
"flume",
2728
+
"futures-util",
2729
+
"metrics",
2730
+
"rand 0.8.5",
2731
+
"serde",
2732
+
"serde_json",
2733
+
"tokio",
2734
+
"tokio-tungstenite",
2735
+
"tracing",
2736
+
"tracing-subscriber",
2737
+
"url",
2738
+
"zstd",
2739
+
]
2740
+
2741
+
[[package]]
2742
+
name = "rusqlite"
2743
+
version = "0.33.0"
2744
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2745
+
checksum = "1c6d5e5acb6f6129fe3f7ba0a7fc77bca1942cb568535e18e7bc40262baf3110"
2746
+
dependencies = [
2747
+
"bitflags",
2748
+
"fallible-iterator",
2749
+
"fallible-streaming-iterator",
2750
+
"hashlink",
2751
+
"libsqlite3-sys",
2752
+
"smallvec",
2753
+
]
2754
+
2755
+
[[package]]
2163
2756
name = "rustc-demangle"
2164
2757
version = "0.1.24"
2165
2758
source = "registry+https://github.com/rust-lang/crates.io-index"
2166
2759
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
2167
2760
2168
2761
[[package]]
2762
+
name = "rustc-hash"
2763
+
version = "2.1.1"
2764
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2765
+
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
2766
+
2767
+
[[package]]
2169
2768
name = "rustc_version"
2170
2769
version = "0.4.1"
2171
2770
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2188
2787
]
2189
2788
2190
2789
[[package]]
2790
+
name = "rustls"
2791
+
version = "0.21.12"
2792
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2793
+
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
2794
+
dependencies = [
2795
+
"log",
2796
+
"ring",
2797
+
"rustls-webpki",
2798
+
"sct",
2799
+
]
2800
+
2801
+
[[package]]
2802
+
name = "rustls-native-certs"
2803
+
version = "0.6.3"
2804
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2805
+
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
2806
+
dependencies = [
2807
+
"openssl-probe",
2808
+
"rustls-pemfile 1.0.4",
2809
+
"schannel",
2810
+
"security-framework",
2811
+
]
2812
+
2813
+
[[package]]
2814
+
name = "rustls-pemfile"
2815
+
version = "1.0.4"
2816
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2817
+
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
2818
+
dependencies = [
2819
+
"base64 0.21.7",
2820
+
]
2821
+
2822
+
[[package]]
2191
2823
name = "rustls-pemfile"
2192
2824
version = "2.2.0"
2193
2825
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2203
2835
checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c"
2204
2836
2205
2837
[[package]]
2838
+
name = "rustls-webpki"
2839
+
version = "0.101.7"
2840
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2841
+
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
2842
+
dependencies = [
2843
+
"ring",
2844
+
"untrusted",
2845
+
]
2846
+
2847
+
[[package]]
2206
2848
name = "rustversion"
2207
2849
version = "1.0.20"
2208
2850
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2217
2859
"actix-web",
2218
2860
"anyhow",
2219
2861
"askama",
2220
-
"async-stream",
2862
+
"async-sqlite",
2863
+
"async-trait",
2221
2864
"atrium-api",
2222
-
"atrium-xrpc-client",
2865
+
"atrium-common",
2866
+
"atrium-identity",
2867
+
"atrium-oauth",
2868
+
"chrono",
2869
+
"dotenv",
2223
2870
"env_logger",
2871
+
"hickory-resolver",
2224
2872
"log",
2873
+
"rocketman",
2225
2874
"serde",
2226
2875
"serde_json",
2876
+
"thiserror",
2877
+
"tokio",
2227
2878
]
2228
2879
2229
2880
[[package]]
···
2254
2905
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
2255
2906
2256
2907
[[package]]
2908
+
name = "sct"
2909
+
version = "0.7.1"
2910
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2911
+
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
2912
+
dependencies = [
2913
+
"ring",
2914
+
"untrusted",
2915
+
]
2916
+
2917
+
[[package]]
2918
+
name = "sec1"
2919
+
version = "0.7.3"
2920
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2921
+
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
2922
+
dependencies = [
2923
+
"base16ct",
2924
+
"der",
2925
+
"generic-array",
2926
+
"subtle",
2927
+
"zeroize",
2928
+
]
2929
+
2930
+
[[package]]
2257
2931
name = "security-framework"
2258
2932
version = "2.11.1"
2259
2933
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2284
2958
2285
2959
[[package]]
2286
2960
name = "serde"
2287
-
version = "1.0.218"
2961
+
version = "1.0.219"
2288
2962
source = "registry+https://github.com/rust-lang/crates.io-index"
2289
-
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
2963
+
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
2290
2964
dependencies = [
2291
2965
"serde_derive",
2292
2966
]
···
2302
2976
2303
2977
[[package]]
2304
2978
name = "serde_derive"
2305
-
version = "1.0.218"
2979
+
version = "1.0.219"
2306
2980
source = "registry+https://github.com/rust-lang/crates.io-index"
2307
-
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
2981
+
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
2308
2982
dependencies = [
2309
2983
"proc-macro2",
2310
2984
"quote",
···
2395
3069
]
2396
3070
2397
3071
[[package]]
3072
+
name = "signature"
3073
+
version = "2.2.0"
3074
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3075
+
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
3076
+
dependencies = [
3077
+
"digest",
3078
+
"rand_core 0.6.4",
3079
+
]
3080
+
3081
+
[[package]]
2398
3082
name = "slab"
2399
3083
version = "0.4.9"
2400
3084
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2420
3104
]
2421
3105
2422
3106
[[package]]
3107
+
name = "spin"
3108
+
version = "0.9.8"
3109
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3110
+
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
3111
+
dependencies = [
3112
+
"lock_api",
3113
+
]
3114
+
3115
+
[[package]]
2423
3116
name = "stable_deref_trait"
2424
3117
version = "1.2.0"
2425
3118
source = "registry+https://github.com/rust-lang/crates.io-index"
2426
3119
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
3120
+
3121
+
[[package]]
3122
+
name = "strsim"
3123
+
version = "0.11.1"
3124
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3125
+
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
2427
3126
2428
3127
[[package]]
2429
3128
name = "subtle"
···
2554
3253
]
2555
3254
2556
3255
[[package]]
3256
+
name = "tinyvec"
3257
+
version = "1.9.0"
3258
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3259
+
checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
3260
+
dependencies = [
3261
+
"tinyvec_macros",
3262
+
]
3263
+
3264
+
[[package]]
3265
+
name = "tinyvec_macros"
3266
+
version = "0.1.1"
3267
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3268
+
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
3269
+
3270
+
[[package]]
2557
3271
name = "tokio"
2558
-
version = "1.44.0"
3272
+
version = "1.44.1"
2559
3273
source = "registry+https://github.com/rust-lang/crates.io-index"
2560
-
checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
3274
+
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
2561
3275
dependencies = [
2562
3276
"backtrace",
2563
3277
"bytes",
···
2567
3281
"pin-project-lite",
2568
3282
"signal-hook-registry",
2569
3283
"socket2",
3284
+
"tokio-macros",
2570
3285
"windows-sys 0.52.0",
2571
3286
]
2572
3287
2573
3288
[[package]]
3289
+
name = "tokio-macros"
3290
+
version = "2.5.0"
3291
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3292
+
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
3293
+
dependencies = [
3294
+
"proc-macro2",
3295
+
"quote",
3296
+
"syn",
3297
+
]
3298
+
3299
+
[[package]]
2574
3300
name = "tokio-native-tls"
2575
3301
version = "0.3.1"
2576
3302
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2581
3307
]
2582
3308
2583
3309
[[package]]
3310
+
name = "tokio-rustls"
3311
+
version = "0.24.1"
3312
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3313
+
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
3314
+
dependencies = [
3315
+
"rustls",
3316
+
"tokio",
3317
+
]
3318
+
3319
+
[[package]]
3320
+
name = "tokio-tungstenite"
3321
+
version = "0.20.1"
3322
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3323
+
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
3324
+
dependencies = [
3325
+
"futures-util",
3326
+
"log",
3327
+
"rustls",
3328
+
"rustls-native-certs",
3329
+
"tokio",
3330
+
"tokio-rustls",
3331
+
"tungstenite",
3332
+
"webpki-roots",
3333
+
]
3334
+
3335
+
[[package]]
2584
3336
name = "tokio-util"
2585
-
version = "0.7.13"
3337
+
version = "0.7.14"
2586
3338
source = "registry+https://github.com/rust-lang/crates.io-index"
2587
-
checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
3339
+
checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034"
2588
3340
dependencies = [
2589
3341
"bytes",
2590
3342
"futures-core",
···
2628
3380
dependencies = [
2629
3381
"log",
2630
3382
"pin-project-lite",
3383
+
"tracing-attributes",
2631
3384
"tracing-core",
2632
3385
]
2633
3386
2634
3387
[[package]]
3388
+
name = "tracing-attributes"
3389
+
version = "0.1.28"
3390
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3391
+
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
3392
+
dependencies = [
3393
+
"proc-macro2",
3394
+
"quote",
3395
+
"syn",
3396
+
]
3397
+
3398
+
[[package]]
2635
3399
name = "tracing-core"
2636
3400
version = "0.1.33"
2637
3401
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2688
3452
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
2689
3453
2690
3454
[[package]]
3455
+
name = "tungstenite"
3456
+
version = "0.20.1"
3457
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3458
+
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
3459
+
dependencies = [
3460
+
"byteorder",
3461
+
"bytes",
3462
+
"data-encoding",
3463
+
"http 0.2.12",
3464
+
"httparse",
3465
+
"log",
3466
+
"rand 0.8.5",
3467
+
"rustls",
3468
+
"sha1",
3469
+
"thiserror",
3470
+
"url",
3471
+
"utf-8",
3472
+
]
3473
+
3474
+
[[package]]
2691
3475
name = "typenum"
2692
3476
version = "1.18.0"
2693
3477
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2728
3512
checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06"
2729
3513
2730
3514
[[package]]
3515
+
name = "untrusted"
3516
+
version = "0.9.0"
3517
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3518
+
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
3519
+
3520
+
[[package]]
2731
3521
name = "url"
2732
3522
version = "2.5.4"
2733
3523
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2737
3527
"idna",
2738
3528
"percent-encoding",
2739
3529
]
3530
+
3531
+
[[package]]
3532
+
name = "utf-8"
3533
+
version = "0.7.6"
3534
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3535
+
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
2740
3536
2741
3537
[[package]]
2742
3538
name = "utf16_iter"
···
2905
3701
]
2906
3702
2907
3703
[[package]]
3704
+
name = "webpki-roots"
3705
+
version = "0.25.4"
3706
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3707
+
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
3708
+
3709
+
[[package]]
3710
+
name = "widestring"
3711
+
version = "1.2.0"
3712
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3713
+
checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
3714
+
3715
+
[[package]]
2908
3716
name = "winapi"
2909
3717
version = "0.3.9"
2910
3718
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2928
3736
2929
3737
[[package]]
2930
3738
name = "windows"
3739
+
version = "0.52.0"
3740
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3741
+
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
3742
+
dependencies = [
3743
+
"windows-core 0.52.0",
3744
+
"windows-targets 0.52.6",
3745
+
]
3746
+
3747
+
[[package]]
3748
+
name = "windows"
2931
3749
version = "0.58.0"
2932
3750
source = "registry+https://github.com/rust-lang/crates.io-index"
2933
3751
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
2934
3752
dependencies = [
2935
3753
"windows-core 0.58.0",
2936
-
"windows-targets",
3754
+
"windows-targets 0.52.6",
2937
3755
]
2938
3756
2939
3757
[[package]]
···
2942
3760
source = "registry+https://github.com/rust-lang/crates.io-index"
2943
3761
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
2944
3762
dependencies = [
2945
-
"windows-targets",
3763
+
"windows-targets 0.52.6",
2946
3764
]
2947
3765
2948
3766
[[package]]
···
2955
3773
"windows-interface",
2956
3774
"windows-result",
2957
3775
"windows-strings",
2958
-
"windows-targets",
3776
+
"windows-targets 0.52.6",
2959
3777
]
2960
3778
2961
3779
[[package]]
···
2994
3812
dependencies = [
2995
3813
"windows-result",
2996
3814
"windows-strings",
2997
-
"windows-targets",
3815
+
"windows-targets 0.52.6",
2998
3816
]
2999
3817
3000
3818
[[package]]
···
3003
3821
source = "registry+https://github.com/rust-lang/crates.io-index"
3004
3822
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
3005
3823
dependencies = [
3006
-
"windows-targets",
3824
+
"windows-targets 0.52.6",
3007
3825
]
3008
3826
3009
3827
[[package]]
···
3013
3831
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
3014
3832
dependencies = [
3015
3833
"windows-result",
3016
-
"windows-targets",
3834
+
"windows-targets 0.52.6",
3835
+
]
3836
+
3837
+
[[package]]
3838
+
name = "windows-sys"
3839
+
version = "0.48.0"
3840
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3841
+
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
3842
+
dependencies = [
3843
+
"windows-targets 0.48.5",
3017
3844
]
3018
3845
3019
3846
[[package]]
···
3022
3849
source = "registry+https://github.com/rust-lang/crates.io-index"
3023
3850
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
3024
3851
dependencies = [
3025
-
"windows-targets",
3852
+
"windows-targets 0.52.6",
3026
3853
]
3027
3854
3028
3855
[[package]]
···
3031
3858
source = "registry+https://github.com/rust-lang/crates.io-index"
3032
3859
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
3033
3860
dependencies = [
3034
-
"windows-targets",
3861
+
"windows-targets 0.52.6",
3862
+
]
3863
+
3864
+
[[package]]
3865
+
name = "windows-targets"
3866
+
version = "0.48.5"
3867
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3868
+
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
3869
+
dependencies = [
3870
+
"windows_aarch64_gnullvm 0.48.5",
3871
+
"windows_aarch64_msvc 0.48.5",
3872
+
"windows_i686_gnu 0.48.5",
3873
+
"windows_i686_msvc 0.48.5",
3874
+
"windows_x86_64_gnu 0.48.5",
3875
+
"windows_x86_64_gnullvm 0.48.5",
3876
+
"windows_x86_64_msvc 0.48.5",
3035
3877
]
3036
3878
3037
3879
[[package]]
···
3040
3882
source = "registry+https://github.com/rust-lang/crates.io-index"
3041
3883
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
3042
3884
dependencies = [
3043
-
"windows_aarch64_gnullvm",
3044
-
"windows_aarch64_msvc",
3045
-
"windows_i686_gnu",
3885
+
"windows_aarch64_gnullvm 0.52.6",
3886
+
"windows_aarch64_msvc 0.52.6",
3887
+
"windows_i686_gnu 0.52.6",
3046
3888
"windows_i686_gnullvm",
3047
-
"windows_i686_msvc",
3048
-
"windows_x86_64_gnu",
3049
-
"windows_x86_64_gnullvm",
3050
-
"windows_x86_64_msvc",
3889
+
"windows_i686_msvc 0.52.6",
3890
+
"windows_x86_64_gnu 0.52.6",
3891
+
"windows_x86_64_gnullvm 0.52.6",
3892
+
"windows_x86_64_msvc 0.52.6",
3051
3893
]
3052
3894
3053
3895
[[package]]
3054
3896
name = "windows_aarch64_gnullvm"
3897
+
version = "0.48.5"
3898
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3899
+
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
3900
+
3901
+
[[package]]
3902
+
name = "windows_aarch64_gnullvm"
3055
3903
version = "0.52.6"
3056
3904
source = "registry+https://github.com/rust-lang/crates.io-index"
3057
3905
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
3058
3906
3059
3907
[[package]]
3060
3908
name = "windows_aarch64_msvc"
3909
+
version = "0.48.5"
3910
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3911
+
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
3912
+
3913
+
[[package]]
3914
+
name = "windows_aarch64_msvc"
3061
3915
version = "0.52.6"
3062
3916
source = "registry+https://github.com/rust-lang/crates.io-index"
3063
3917
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
3918
+
3919
+
[[package]]
3920
+
name = "windows_i686_gnu"
3921
+
version = "0.48.5"
3922
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3923
+
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
3064
3924
3065
3925
[[package]]
3066
3926
name = "windows_i686_gnu"
···
3076
3936
3077
3937
[[package]]
3078
3938
name = "windows_i686_msvc"
3939
+
version = "0.48.5"
3940
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3941
+
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
3942
+
3943
+
[[package]]
3944
+
name = "windows_i686_msvc"
3079
3945
version = "0.52.6"
3080
3946
source = "registry+https://github.com/rust-lang/crates.io-index"
3081
3947
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
3082
3948
3083
3949
[[package]]
3084
3950
name = "windows_x86_64_gnu"
3951
+
version = "0.48.5"
3952
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3953
+
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
3954
+
3955
+
[[package]]
3956
+
name = "windows_x86_64_gnu"
3085
3957
version = "0.52.6"
3086
3958
source = "registry+https://github.com/rust-lang/crates.io-index"
3087
3959
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
3088
3960
3089
3961
[[package]]
3090
3962
name = "windows_x86_64_gnullvm"
3963
+
version = "0.48.5"
3964
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3965
+
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
3966
+
3967
+
[[package]]
3968
+
name = "windows_x86_64_gnullvm"
3091
3969
version = "0.52.6"
3092
3970
source = "registry+https://github.com/rust-lang/crates.io-index"
3093
3971
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
3094
3972
3095
3973
[[package]]
3096
3974
name = "windows_x86_64_msvc"
3975
+
version = "0.48.5"
3976
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3977
+
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
3978
+
3979
+
[[package]]
3980
+
name = "windows_x86_64_msvc"
3097
3981
version = "0.52.6"
3098
3982
source = "registry+https://github.com/rust-lang/crates.io-index"
3099
3983
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
3100
3984
3101
3985
[[package]]
3986
+
name = "winnow"
3987
+
version = "0.7.4"
3988
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3989
+
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36"
3990
+
dependencies = [
3991
+
"memchr",
3992
+
]
3993
+
3994
+
[[package]]
3995
+
name = "winreg"
3996
+
version = "0.50.0"
3997
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3998
+
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
3999
+
dependencies = [
4000
+
"cfg-if",
4001
+
"windows-sys 0.48.0",
4002
+
]
4003
+
4004
+
[[package]]
3102
4005
name = "wit-bindgen-rt"
3103
4006
version = "0.33.0"
3104
4007
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3150
4053
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
3151
4054
dependencies = [
3152
4055
"byteorder",
3153
-
"zerocopy-derive",
4056
+
"zerocopy-derive 0.7.35",
4057
+
]
4058
+
4059
+
[[package]]
4060
+
name = "zerocopy"
4061
+
version = "0.8.24"
4062
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4063
+
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
4064
+
dependencies = [
4065
+
"zerocopy-derive 0.8.24",
3154
4066
]
3155
4067
3156
4068
[[package]]
···
3165
4077
]
3166
4078
3167
4079
[[package]]
4080
+
name = "zerocopy-derive"
4081
+
version = "0.8.24"
4082
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4083
+
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
4084
+
dependencies = [
4085
+
"proc-macro2",
4086
+
"quote",
4087
+
"syn",
4088
+
]
4089
+
4090
+
[[package]]
3168
4091
name = "zerofrom"
3169
4092
version = "0.1.6"
3170
4093
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3183
4106
"quote",
3184
4107
"syn",
3185
4108
"synstructure",
4109
+
]
4110
+
4111
+
[[package]]
4112
+
name = "zeroize"
4113
+
version = "1.8.1"
4114
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4115
+
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
4116
+
dependencies = [
4117
+
"serde",
3186
4118
]
3187
4119
3188
4120
[[package]]
+24
-8
Cargo.toml
+24
-8
Cargo.toml
···
4
4
edition = "2024"
5
5
6
6
[dependencies]
7
-
askama = "0.12"
8
7
actix-files = "0.6.6"
9
8
actix-session = { version = "0.10", features = ["cookie-session"] }
10
-
actix-web = "4.9"
11
-
serde = { version = "1.0", features = ["derive"] }
12
-
log = "0.4"
13
-
async-stream = "0.3"
14
-
env_logger = "0.11"
9
+
actix-web = "4.10.2"
15
10
anyhow = "1.0.97"
16
-
serde_json = "1.0.140"
11
+
askama = "0.13"
12
+
atrium-common = "0.1.1"
17
13
atrium-api = "0.25.0"
18
-
atrium-xrpc-client = "0.5.10"
14
+
atrium-identity = "0.1.3"
15
+
atrium-oauth = "0.1.0"
16
+
chrono = "0.4.40"
17
+
env_logger = "0.11.7"
18
+
hickory-resolver = "0.24.1"
19
+
log = "0.4.27"
20
+
serde = { version = "1.0.219", features = ["derive"] }
21
+
serde_json = "1.0.140"
22
+
rocketman = "0.2.0"
23
+
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
24
+
dotenv = "0.15.0"
25
+
thiserror = "1.0.69"
26
+
async-sqlite = "0.5.0"
27
+
async-trait = "0.1.88"
28
+
29
+
[build-dependencies]
30
+
askama = "0.13"
31
+
32
+
33
+
[profile.dev.package.askama_derive]
34
+
opt-level = 3
+21
LICENSE
+21
LICENSE
···
1
+
MIT License
2
+
3
+
Copyright (c) 2025 Bailey Townsend
4
+
5
+
Permission is hereby granted, free of charge, to any person obtaining a copy
6
+
of this software and associated documentation files (the "Software"), to deal
7
+
in the Software without restriction, including without limitation the rights
8
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+
copies of the Software, and to permit persons to whom the Software is
10
+
furnished to do so, subject to the following conditions:
11
+
12
+
The above copyright notice and this permission notice shall be included in all
13
+
copies or substantial portions of the Software.
14
+
15
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+
SOFTWARE.
+1061
-5
README.md
+1061
-5
README.md
···
1
-
# Dev commands
1
+
# Rusty Statusphere
2
+
3
+
Originally taken
4
+
from [bluesky-social/atproto-website](https://github.com/bluesky-social/atproto-website/blob/dbcd70ced53078579c7e5b015a26db295b7a7807/src/app/%5Blocale%5D/guides/applications/en.mdx)
5
+
6
+
> [!NOTE]
7
+
> ***This tutorial is based off of the original quick start guide found [here](https://atproto.com/guides/applications).
8
+
> The goal is to follow as closely to the original as possible, expect for one small change. It's in Rust ๐ฆ.
9
+
> All credit goes to the maintainers of the original project and tutorial. This was made to help you get started with
10
+
> using Rust to write applications in the Atmosphere. Parts that stray from the tutorial, or need extra context will be in blocks like this one.***
11
+
12
+
# Quick start guide to building applications on AT Protocol
13
+
14
+
[Find the source code on GitHub](https://github.com/fatfingers23/rusty_statusphere_example_app)
15
+
16
+
In this guide, we're going to build a simple multi-user app that publishes your current "status" as an emoji. Our
17
+
application will look like this:
18
+
19
+

20
+
21
+
We will cover how to:
22
+
23
+
- Signin via OAuth
24
+
- Fetch information about users (profiles)
25
+
- Listen to the network firehose for new data via the [Jetstream](https://docs.bsky.app/blog/jetstream)
26
+
- Publish data on the user's account using a custom schema
27
+
28
+
We're going to keep this light so you can quickly wrap your head around ATProto. There will be links with more
29
+
information about each step.
30
+
31
+
## Introduction
32
+
33
+
Data in the Atmosphere is stored on users' personal repos. It's almost like each user has their own website. Our goal is
34
+
to aggregate data from the users into our SQLite DB.
35
+
36
+
Think of our app like a Google. If Google's job was to say which emoji each website had under `/status.json`, then it
37
+
would show something like:
38
+
39
+
- `nytimes.com` is feeling ๐ฐ according to `https://nytimes.com/status.json`
40
+
- `bsky.app` is feeling ๐ฆ according to `https://bsky.app/status.json`
41
+
- `reddit.com` is feeling ๐ค according to `https://reddit.com/status.json`
42
+
43
+
The Atmosphere works the same way, except we're going to check `at://` instead of `https://`. Each user has a data repo
44
+
under an `at://` URL. We'll crawl all the user data repos in the Atmosphere for all the "status.json" records and
45
+
aggregate them into our SQLite database.
46
+
47
+
> `at://` is the URL scheme of the AT Protocol. Under the hood it uses common tech like HTTP and DNS, but it adds all of
48
+
> the features we'll be using in this tutorial.
49
+
50
+
## Step 1. Starting with our Actix Web app
51
+
52
+
Start by cloning the repo and installing packages.
53
+
54
+
```bash
55
+
git clone https://github.com/fatfingers23/rusty_statusphere_example_app.git
56
+
cd rusty_statusphere_example_app
57
+
cp .env.template .env
58
+
cargo run
59
+
# Navigate to http://127.0.0.1:8080
60
+
```
61
+
62
+
Our repo is a regular Web app. We're rendering our HTML server-side like it's 1999. We also have a SQLite database that
63
+
we're managing with [async-sqlite](https://crates.io/crates/async-sqlite).
64
+
65
+
Our starting stack:
66
+
67
+
- [Rust](https://www.rust-lang.org/tools/install)
68
+
- Rust web server ([Actix Web](https://actix.rs/))
69
+
- SQLite database ([async-sqlite](https://crates.io/crates/async-sqlite))
70
+
- HTML Templating ([askama](https://crates.io/crates/askama))
71
+
72
+
> [!NOTE]
73
+
> Along with the above, we are also using a couple of community maintained projects for using rust with the ATProtocol.
74
+
> Since these are community maintained I have also linked sponsor links for the maintainers and _highly_ recommend you to
75
+
> think
76
+
> about sponsoring them.
77
+
> Thanks to their work and projects, we are able to create Rust applications in the Atmosphere.
78
+
> - ATProtocol client and OAuth
79
+
with [atrium](https://github.com/atrium-rs/atrium) - [sponsor sugyan](https://github.com/sponsors/sugyan)
80
+
> - Jetstream consumer
81
+
with [rocketman](https://crates.io/crates/rocketman)- [buy natalie a coffee](https://ko-fi.com/uxieq)
82
+
83
+
With each step we'll explain how our Web app taps into the Atmosphere. Refer to the codebase for more detailed code
84
+
— again, this tutorial is going to keep it light and quick to digest.
85
+
86
+
## Step 2. Signing in with OAuth
87
+
88
+
When somebody logs into our app, they'll give us read & write access to their personal `at://` repo. We'll use that to
89
+
write the status json record.
90
+
91
+
We're going to accomplish this using OAuth ([spec](https://github.com/bluesky-social/proposals/tree/main/0004-oauth)).
92
+
Most of the OAuth flows are going to be handled for us using
93
+
the [atrium-oauth](https://crates.io/crates/atrium-oauth)
94
+
crate. This is the arrangement we're aiming toward:
95
+
96
+

97
+
98
+
When the user logs in, the OAuth client will create a new session with their repo server and give us read/write access
99
+
along with basic user info.
100
+
101
+

102
+
103
+
Our login page just asks the user for their "handle," which is the domain name associated with their account.
104
+
For [Bluesky](https://bsky.app) users, these tend to look like `alice.bsky.social`, but they can be any kind of domain (
105
+
eg `alice.com`).
106
+
107
+
```html
108
+
<!-- templates/login.html -->
109
+
<form action="/login" method="post" class="login-form">
110
+
<input
111
+
type="text"
112
+
name="handle"
113
+
placeholder="Enter your handle (eg alice.bsky.social)"
114
+
required
115
+
/>
116
+
<button type="submit">Log in</button>
117
+
</form>
118
+
```
119
+
120
+
When they submit the form, we tell our OAuth client to initiate the authorization flow and then redirect the user to
121
+
their server to complete the process.
122
+
123
+
```rust
124
+
/** ./src/main.rs **/
125
+
/// Login endpoint
126
+
#[post("/login")]
127
+
async fn login_post(
128
+
request: HttpRequest,
129
+
params: web::Form<LoginForm>,
130
+
oauth_client: web::Data<OAuthClientType>,
131
+
) -> HttpResponse {
132
+
// This will act the same as the js method isValidHandle
133
+
match atrium_api::types::string::Handle::new(params.handle.clone()) {
134
+
Ok(handle) => {
135
+
// Initiates the OAuth flow
136
+
let oauth_url = oauth_client
137
+
.authorize(
138
+
&handle,
139
+
AuthorizeOptions {
140
+
scopes: vec![
141
+
Scope::Known(KnownScope::Atproto),
142
+
Scope::Known(KnownScope::TransitionGeneric),
143
+
],
144
+
..Default::default()
145
+
},
146
+
)
147
+
.await;
148
+
match oauth_url {
149
+
Ok(url) => Redirect::to(url)
150
+
.see_other()
151
+
.respond_to(&request)
152
+
.map_into_boxed_body(),
153
+
Err(err) => {
154
+
log::error!("Error: {err}");
155
+
let html = LoginTemplate {
156
+
title: "Log in",
157
+
error: Some("OAuth error"),
158
+
};
159
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
160
+
}
161
+
}
162
+
}
163
+
Err(err) => {
164
+
let html: LoginTemplate<'_> = LoginTemplate {
165
+
title: "Log in",
166
+
error: Some(err),
167
+
};
168
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
169
+
}
170
+
}
171
+
}
172
+
```
173
+
174
+
This is the same kind of SSO flow that Google or GitHub uses. The user will be asked for their password, then asked to
175
+
confirm the session with your application.
176
+
177
+
When that finishes, the user will be sent back to `/oauth/callback` on our Web app. The OAuth client will store the
178
+
access tokens for the user's server, and then we attach their account's [DID](https://atproto.com/specs/did) to the
179
+
cookie-session.
180
+
181
+
```rust
182
+
/** ./src/main.rs **/
183
+
/// OAuth callback endpoint to complete session creation
184
+
#[get("/oauth/callback")]
185
+
async fn oauth_callback(
186
+
request: HttpRequest,
187
+
params: web::Query<CallbackParams>,
188
+
oauth_client: web::Data<OAuthClientType>,
189
+
session: Session,
190
+
) -> HttpResponse {
191
+
// Store the credentials
192
+
match oauth_client.callback(params.into_inner()).await {
193
+
Ok((bsky_session, _)) => {
194
+
let agent = Agent::new(bsky_session);
195
+
match agent.did().await {
196
+
Some(did) => {
197
+
//Attach the account DID to our user via a cookie
198
+
session.insert("did", did).unwrap();
199
+
Redirect::to("/")
200
+
.see_other()
201
+
.respond_to(&request)
202
+
.map_into_boxed_body()
203
+
}
204
+
None => {
205
+
let html = ErrorTemplate {
206
+
title: "Log in",
207
+
error: "The OAuth agent did not return a DID. My try relogging in.",
208
+
};
209
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
210
+
}
211
+
}
212
+
}
213
+
Err(err) => {
214
+
log::error!("Error: {err}");
215
+
let html = ErrorTemplate {
216
+
title: "Log in",
217
+
error: "OAuth error, check the logs",
218
+
};
219
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
220
+
}
221
+
}
222
+
}
223
+
```
224
+
225
+
With that, we're in business! We now have a session with the user's repo server and can use that to access their data.
226
+
227
+
## Step 3. Fetching the user's profile
228
+
229
+
Why don't we learn something about our user? In [Bluesky](https://bsky.app), users publish a "profile" record which
230
+
looks like this:
231
+
232
+
```rust
233
+
pub struct ProfileViewDetailedData {
234
+
pub display_name: Option<String>, // a human friendly name
235
+
pub description: Option<String>, // a short bio
236
+
pub avatar: Option<String>, // small profile picture
237
+
pub banner: Option<String>, // banner image to put on profiles
238
+
pub created_at: Option<String> // declared time this profile data was added
239
+
// ...
240
+
}
241
+
```
242
+
243
+
You can examine this record directly using [atproto-browser.vercel.app](https://atproto-browser.vercel.app). For
244
+
instance, [this is the profile record for @bsky.app](https://atproto-browser.vercel.app/at?u=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.actor.profile/self).
245
+
246
+
> [!NOTE]
247
+
> In the original tutorial `agent.com.atproto.repo.getRecord` is used, which is
248
+
> this [method](https://docs.rs/atrium-api/latest/atrium_api/com/atproto/repo/get_record/index.html) in atrium-api.
249
+
> For simplicity we are
250
+
> using [agent.api.app.bsky.actor.get_profile](https://docs.rs/atrium-api/latest/atrium_api/app/bsky/actor/get_profile/index.html).
251
+
> The original text found here has been moved to [Step 4. Reading & writing records](#step-4-reading--writing-records)
252
+
> since it makes more sense in that context.
253
+
254
+
We're going to use the [Agent](https://crates.io/crates/atrium-oauth) associated with the
255
+
user's OAuth session to fetch this record.
256
+
257
+
Let's update our homepage to fetch this profile record:
258
+
259
+
```rust
260
+
/** ./src/main.rs **/
261
+
/// Homepage
262
+
#[get("/")]
263
+
async fn home(
264
+
_req: HttpRequest,
265
+
session: Session,
266
+
oauth_client: web::Data<OAuthClientType>,
267
+
db_pool: web::Data<Pool>,
268
+
handle_resolver: web::Data<HandleResolver>,
269
+
) -> Result<impl Responder> {
270
+
const TITLE: &str = "Home";
271
+
272
+
// If the user is signed in, get an agent which communicates with their server
273
+
match session.get::<String>("did").unwrap_or(None) {
274
+
Some(did) => {
275
+
let did = Did::new(did).expect("failed to parse did");
276
+
match oauth_client.restore(&did).await {
277
+
Ok(session) => {
278
+
let agent = Agent::new(session);
279
+
280
+
// Fetch additional information about the logged-in user
281
+
let profile = agent
282
+
.api
283
+
.app
284
+
.bsky
285
+
.actor
286
+
.get_profile(
287
+
atrium_api::app::bsky::actor::get_profile::ParametersData {
288
+
actor: atrium_api::types::string::AtIdentifier::Did(did),
289
+
}.into(),
290
+
)
291
+
.await;
292
+
293
+
// Serve the logged-in view
294
+
let html = HomeTemplate {
295
+
title: TITLE,
296
+
status_options: &STATUS_OPTIONS,
297
+
profile: match profile {
298
+
Ok(profile) => {
299
+
let profile_data = Profile {
300
+
did: profile.did.to_string(),
301
+
display_name: profile.display_name.clone(),
302
+
};
303
+
Some(profile_data)
304
+
}
305
+
Err(err) => {
306
+
log::error!("Error accessing profile: {err}");
307
+
None
308
+
}
309
+
},
310
+
}.render().expect("template should be valid");
311
+
312
+
Ok(web::Html::new(html))
313
+
}
314
+
Err(err) => {
315
+
//Unset the session
316
+
session.remove("did");
317
+
log::error!("Error restoring session: {err}");
318
+
let error_html = ErrorTemplate {
319
+
title: TITLE,
320
+
error: "Was an error resuming the session, please check the logs.",
321
+
}.render().expect("template should be valid");
322
+
323
+
Ok(web::Html::new(error_html))
324
+
}
325
+
}
326
+
}
327
+
None => {
328
+
// Serve the logged-out view
329
+
let html = HomeTemplate {
330
+
title: TITLE,
331
+
status_options: &STATUS_OPTIONS,
332
+
profile: None,
333
+
}.render().expect("template should be valid");
334
+
335
+
Ok(web::Html::new(html))
336
+
}
337
+
}
338
+
}
339
+
```
2
340
3
-
tailwinds
4
-
watchexec -w templates -r ~/Applications/tailwindcss --input public/css/base.css --output public/css/style.css -m
341
+
With that data, we can give a nice personalized welcome banner for our user:
342
+
343
+

5
344
6
-
watch actix
7
-
watchexec -w templates -w src -r cargo run
345
+
```html
346
+
<!-- templates/home.html -->
347
+
<div class="card">
348
+
{% if let Some(Profile {did, display_name}) = profile %}
349
+
<form action="/logout" method="post" class="session-form">
350
+
<div>
351
+
Hi,
352
+
{% if let Some(display_name) = display_name %}
353
+
<strong>{{display_name}}</strong>
354
+
{% else %}
355
+
<strong>friend</strong>
356
+
{% endif %}.
357
+
What's your status today??
358
+
</div>
359
+
<div>
360
+
<button type="submit">Log out</button>
361
+
</div>
362
+
</form>
363
+
{% else %}
364
+
<div class="session-form">
365
+
<div><a href="/login">Log in</a> to set your status!</div>
366
+
<div>
367
+
<a href="/login" class="button">Log in</a>
368
+
</div>
369
+
</div>
370
+
{% endif %}
371
+
</div>
372
+
```
373
+
374
+
## Step 4. Reading & writing records
375
+
376
+
You can think of the user repositories as collections of JSON records:
377
+
378
+

379
+
380
+
When asking for a record, we provide three pieces of information.
381
+
382
+
- **repo** The [DID](https://atproto.com/specs/did) which identifies the user,
383
+
- **collection** The collection name, and
384
+
- **rkey** The record key
385
+
386
+
We'll explain the collection name shortly. Record keys are strings
387
+
with [some restrictions](https://atproto.com/specs/record-key#record-key-syntax) and a couple of common patterns. The
388
+
`"self"` pattern is used when a collection is expected to only contain one record which describes the user.
389
+
390
+
Let's look again at how we read the "profile" record:
391
+
392
+
```rust
393
+
fn example_get_record() {
394
+
let get_result = agent
395
+
.api
396
+
.com
397
+
.atproto
398
+
.repo
399
+
.get_record(
400
+
atrium_api::com::atproto::repo::get_record::ParametersData {
401
+
cid: None,
402
+
collection: "app.bsky.actor.profile" // The collection
403
+
.parse()
404
+
.unwrap(),
405
+
repo: did.into(), // The user
406
+
rkey: "self".parse().unwrap(), // The record key
407
+
}
408
+
.into(),
409
+
)
410
+
.await;
411
+
}
412
+
413
+
```
414
+
415
+
We write records using a similar API. Since our goal is to write "status" records, let's look at how that will happen:
416
+
417
+
```rust
418
+
fn example_create_record() {
419
+
let did = atrium_api::types::string::Did::new(did_string.clone()).unwrap();
420
+
let agent = Agent::new(session);
421
+
422
+
let status: Unknown = serde_json::from_str(
423
+
format!(
424
+
r#"{{"$type":"xyz.statusphere.status","status":"{}","createdAt":"{}"}}"#,
425
+
form.status,
426
+
Datetime::now().as_str()
427
+
)
428
+
.as_str(),
429
+
).unwrap();
430
+
431
+
let create_result = agent
432
+
.api
433
+
.com
434
+
.atproto
435
+
.repo
436
+
.create_record(
437
+
atrium_api::com::atproto::repo::create_record::InputData {
438
+
collection: Status::NSID.parse().unwrap(), // The collection
439
+
repo: did.clone().into(), // The user
440
+
rkey: None, // The record key, auto creates with None
441
+
record: status, // The record from a strong type
442
+
swap_commit: None,
443
+
validate: None,
444
+
}
445
+
.into(),
446
+
)
447
+
.await;
448
+
}
449
+
```
450
+
451
+
Our `POST /status` route is going to use this API to publish the user's status to their repo.
452
+
453
+
```rust
454
+
/// "Set status" Endpoint
455
+
#[post("/status")]
456
+
async fn status(
457
+
request: HttpRequest,
458
+
session: Session,
459
+
oauth_client: web::Data<OAuthClientType>,
460
+
db_pool: web::Data<Pool>,
461
+
form: web::Form<StatusForm>,
462
+
) -> HttpResponse {
463
+
const TITLE: &str = "Home";
464
+
465
+
// If the user is signed in, get an agent which communicates with their server
466
+
match session.get::<String>("did").unwrap_or(None) {
467
+
Some(did_string) => {
468
+
let did = atrium_api::types::string::Did::new(did_string.clone())
469
+
.expect("failed to parse did");
470
+
match oauth_client.restore(&did).await {
471
+
Ok(session) => {
472
+
let agent = Agent::new(session);
473
+
474
+
// Construct their status record
475
+
let status: Unknown = serde_json::from_str(
476
+
format!(
477
+
r#"{{"$type":"xyz.statusphere.status","status":"{}","createdAt":"{}"}}"#,
478
+
form.status,
479
+
Datetime::now().as_str()
480
+
)
481
+
.as_str(),
482
+
).unwrap();
483
+
484
+
// Write the status record to the user's repository
485
+
let create_result = agent
486
+
.api
487
+
.com
488
+
.atproto
489
+
.repo
490
+
.create_record(
491
+
atrium_api::com::atproto::repo::create_record::InputData {
492
+
collection: "xyz.statusphere.status".parse().unwrap(),
493
+
repo: did.clone().into(),
494
+
rkey: None,
495
+
record: status,
496
+
swap_commit: None,
497
+
validate: None,
498
+
}
499
+
.into(),
500
+
)
501
+
.await;
502
+
503
+
match create_result {
504
+
Ok(_) => Redirect::to("/")
505
+
.see_other()
506
+
.respond_to(&request)
507
+
.map_into_boxed_body(),
508
+
Err(err) => {
509
+
log::error!("Error creating status: {err}");
510
+
let error_html = ErrorTemplate {
511
+
title: TITLE,
512
+
error: "Was an error creating the status, please check the logs.",
513
+
}
514
+
.render()
515
+
.expect("template should be valid");
516
+
HttpResponse::Ok().body(error_html)
517
+
}
518
+
}
519
+
}
520
+
Err(err) => {
521
+
//Unset the session
522
+
session.remove("did");
523
+
log::error!(
524
+
"Error restoring session, we are removing the session from the cookie: {err}"
525
+
);
526
+
let error_html = ErrorTemplate {
527
+
title: TITLE,
528
+
error: "Was an error resuming the session, please check the logs.",
529
+
}
530
+
.render()
531
+
.expect("template should be valid");
532
+
HttpResponse::Ok().body(error_html)
533
+
}
534
+
}
535
+
}
536
+
None => {
537
+
let error_template = ErrorTemplate {
538
+
title: "Error",
539
+
error: "You must be logged in to create a status.",
540
+
}
541
+
.render()
542
+
.expect("template should be valid");
543
+
HttpResponse::Ok().body(error_template)
544
+
}
545
+
}
546
+
}
547
+
```
548
+
549
+
Now in our homepage we can list out the status buttons:
550
+
551
+
```html
552
+
<!-- templates/home.html -->
553
+
<form action="/status" method="post" class="status-options">
554
+
{% for status in status_options %}
555
+
<button
556
+
class="{% if let Some(my_status) = my_status %} {%if my_status == status %} status-option selected {% else %} status-option {% endif %} {% else %} status-option {%endif%} "
557
+
name="status" value="{{status}}">
558
+
{{status}}
559
+
</button>
560
+
{% endfor %}
561
+
</form>
562
+
```
563
+
564
+
And here we are!
565
+
566
+

567
+
568
+
## Step 5. Creating a custom "status" schema
569
+
570
+
Repo collections are typed, meaning that they have a defined schema. The `app.bsky.actor.profile` type
571
+
definition [can be found here](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/actor/profile.json).
572
+
573
+
Anybody can create a new schema using the [Lexicon](https://atproto.com/specs/lexicon) language, which is very similar
574
+
to [JSON-Schema](http://json-schema.org/). The schemas use [reverse-DNS IDs](https://atproto.com/specs/nsid) which
575
+
indicate ownership. In this demo app we're going to use `xyz.statusphere` which we registered specifically for this
576
+
project (aka statusphere.xyz).
577
+
578
+
> ### Why create a schema?
579
+
>
580
+
> Schemas help other applications understand the data your app is creating. By publishing your schemas, you make it
581
+
> easier for other application authors to publish data in a format your app will recognize and handle.
582
+
583
+
Let's create our schema in the `/lexicons` folder of our codebase. You
584
+
can [read more about how to define schemas here](https://atproto.com/guides/lexicon).
585
+
586
+
```json
587
+
/** lexicons/status.json **/
588
+
{
589
+
"lexicon": 1,
590
+
"id": "xyz.statusphere.status",
591
+
"defs": {
592
+
"main": {
593
+
"type": "record",
594
+
"key": "tid",
595
+
"record": {
596
+
"type": "object",
597
+
"required": [
598
+
"status",
599
+
"createdAt"
600
+
],
601
+
"properties": {
602
+
"status": {
603
+
"type": "string",
604
+
"minLength": 1,
605
+
"maxGraphemes": 1,
606
+
"maxLength": 32
607
+
},
608
+
"createdAt": {
609
+
"type": "string",
610
+
"format": "datetime"
611
+
}
612
+
}
613
+
}
614
+
}
615
+
}
616
+
}
617
+
```
618
+
619
+
Now let's run some code-generation using our schema:
620
+
621
+
> [!NOTE]
622
+
> For generating schemas, we are going to
623
+
> use [esquema-cli](https://github.com/fatfingers23/esquema?tab=readme-ov-file)
624
+
> (Which is a tool I've created from a fork of atrium's codegen).
625
+
> This can be installed by running this command
626
+
`cargo install esquema-cli --git https://github.com/fatfingers23/esquema.git`
627
+
> This is a WIP tool with bugs and missing features. But it's good enough for us to generate Rust types from the lexicon
628
+
> schema.
629
+
630
+
```bash
631
+
esquema-cli generate local -l ./lexicons/ -o ./src/ --module lexicons
632
+
```
633
+
634
+
635
+
636
+
This will produce Rust structs. Here's what that generated code looks like:
637
+
638
+
```rust
639
+
/** ./src/lexicons/xyz/statusphere/status.rs **/
640
+
// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
641
+
//!Definitions for the `xyz.statusphere.status` namespace.
642
+
use atrium_api::types::TryFromUnknown;
643
+
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
644
+
#[serde(rename_all = "camelCase")]
645
+
pub struct RecordData {
646
+
pub created_at: atrium_api::types::string::Datetime,
647
+
pub status: String,
648
+
}
649
+
pub type Record = atrium_api::types::Object<RecordData>;
650
+
impl From<atrium_api::types::Unknown> for RecordData {
651
+
fn from(value: atrium_api::types::Unknown) -> Self {
652
+
Self::try_from_unknown(value).unwrap()
653
+
}
654
+
}
655
+
656
+
```
657
+
658
+
> [!NOTE]
659
+
> You may have noticed we do not cover the validation part like in the TypeScript version.
660
+
> Esquema can validate to a point such as the data structure and if a field is there or not.
661
+
> But validation of the data itself is not possible, yet.
662
+
> There are plans to add it.
663
+
> Maybe you would like to add it?
664
+
> https://github.com/fatfingers23/esquema/issues/3
665
+
666
+
Let's use that code to improve the `POST /status` route:
667
+
668
+
```rust
669
+
/// "Set status" Endpoint
670
+
#[post("/status")]
671
+
async fn status(
672
+
request: HttpRequest,
673
+
session: Session,
674
+
oauth_client: web::Data<OAuthClientType>,
675
+
db_pool: web::Data<Pool>,
676
+
form: web::Form<StatusForm>,
677
+
) -> HttpResponse {
678
+
// ...
679
+
let agent = Agent::new(session);
680
+
//We use the new status type we generated with esquema
681
+
let status: KnownRecord = lexicons::xyz::statusphere::status::RecordData {
682
+
created_at: Datetime::now(),
683
+
status: form.status.clone(),
684
+
}
685
+
.into();
686
+
687
+
// TODO no validation yet from esquema
688
+
// Maybe you'd like to add it? https://github.com/fatfingers23/esquema/issues/3
689
+
690
+
let create_result = agent
691
+
.api
692
+
.com
693
+
.atproto
694
+
.repo
695
+
.create_record(
696
+
atrium_api::com::atproto::repo::create_record::InputData {
697
+
collection: Status::NSID.parse().unwrap(),
698
+
repo: did.into(),
699
+
rkey: None,
700
+
record: status.into(),
701
+
swap_commit: None,
702
+
validate: None,
703
+
}
704
+
.into(),
705
+
)
706
+
.await;
707
+
// ...
708
+
}
709
+
```
710
+
> [!NOTE]
711
+
> You will notice the first example used a string to serialize to Unknown, you could do something similar with
712
+
> a struct you create, then serialize.But I created esquema to make that easier.
713
+
> With esquema you can use other provided lexicons
714
+
> or ones you create to build out the data structure for your ATProtocol application.
715
+
> As well as in future updates it will honor the
716
+
> validation you have in the Lexicon.
717
+
> Things like string should be 10 long, etc.
718
+
719
+
## Step 6. Listening to the firehose
720
+
721
+
> [!IMPORTANT]
722
+
> It is important to note that the original tutorial they connect directly to the firehose, but in this one we use
723
+
> [rocketman](https://crates.io/crates/rocketman) to connect to the Jetstream instead.
724
+
> For most use cases this is fine and usually easier when using other clients than the Bluesky provided ones.
725
+
> But it is important to note there are some differences that can
726
+
> be found in their introduction to Jetstream article.
727
+
> https://docs.bsky.app/blog/jetstream#tradeoffs-and-use-cases
728
+
729
+
So far, we have:
730
+
731
+
- Logged in via OAuth
732
+
- Created a custom schema
733
+
- Read & written records for the logged in user
734
+
735
+
Now we want to fetch the status records from other users.
736
+
737
+
Remember how we referred to our app as being like Google, crawling around the repos to get their records? One advantage
738
+
we have in the AT Protocol is that each repo publishes an event log of their updates.
739
+
740
+

741
+
742
+
Using a [~~Relay~~ Jetstream service](https://docs.bsky.app/blog/jetstream) we can listen to an
743
+
aggregated firehose of these events across all users in the network. In our case what we're looking for are valid
744
+
`xyz.statusphere.status` records.
745
+
746
+
```rust
747
+
/** ./src/ingester.rs **/
748
+
#[async_trait]
749
+
impl LexiconIngestor for StatusSphereIngester {
750
+
async fn ingest(&self, message: Event<Value>) -> anyhow::Result<()> {
751
+
if let Some(commit) = &message.commit {
752
+
//We manually construct the uri since jetstream does not provide it
753
+
//at://{users did}/{collection: xyz.statusphere.status}{records key}
754
+
let record_uri = format!("at://{}/{}/{}", message.did, commit.collection, commit.rkey);
755
+
match commit.operation {
756
+
Operation::Create | Operation::Update => {
757
+
if let Some(record) = &commit.record {
758
+
//We deserialize the record into our Rust struct
759
+
let status_at_proto_record = serde_json::from_value::<
760
+
lexicons::xyz::statusphere::status::RecordData,
761
+
>(record.clone())?;
762
+
763
+
if let Some(ref _cid) = commit.cid {
764
+
// Although esquema does not have full validation yet,
765
+
// if you get to this point,
766
+
// You know the data structure is the same
767
+
768
+
// Store the status
769
+
// TODO
770
+
}
771
+
}
772
+
}
773
+
Operation::Delete => {},
774
+
}
775
+
} else {
776
+
return Err(anyhow!("Message has no commit"));
777
+
}
778
+
Ok(())
779
+
}
780
+
}
781
+
```
782
+
783
+
Let's create a SQLite table to store these statuses:
784
+
785
+
```rust
786
+
/** ./src/db.rs **/
787
+
// Create our statuses table
788
+
pub async fn create_tables_in_database(pool: &Pool) -> Result<(), async_sqlite::Error> {
789
+
pool.conn(move |conn| {
790
+
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
791
+
792
+
// status
793
+
conn.execute(
794
+
"CREATE TABLE IF NOT EXISTS status (
795
+
uri TEXT PRIMARY KEY,
796
+
authorDid TEXT NOT NULL,
797
+
status TEXT NOT NULL,
798
+
createdAt INTEGER NOT NULL,
799
+
indexedAt INTEGER NOT NULL
800
+
)",
801
+
[],
802
+
)
803
+
.unwrap();
804
+
805
+
// ...
806
+
```
807
+
808
+
Now we can write these statuses into our database as they arrive from the firehose:
809
+
810
+
```rust
811
+
/** ./src/ingester.rs **/
812
+
// If the write is a valid status update
813
+
if let Some(record) = &commit.record {
814
+
let status_at_proto_record = serde_json::from_value::<
815
+
lexicons::xyz::statusphere::status::RecordData,
816
+
>(record.clone())?;
817
+
818
+
if let Some(ref _cid) = commit.cid {
819
+
// Although esquema does not have full validation yet,
820
+
// if you get to this point,
821
+
// You know the data structure is the same
822
+
let created = status_at_proto_record.created_at.as_ref();
823
+
let right_now = chrono::Utc::now();
824
+
// We save or update the record in the db
825
+
StatusFromDb {
826
+
uri: record_uri,
827
+
author_did: message.did.clone(),
828
+
status: status_at_proto_record.status.clone(),
829
+
created_at: created.to_utc(),
830
+
indexed_at: right_now,
831
+
handle: None,
832
+
}
833
+
.save_or_update(&self.db_pool)
834
+
.await?;
835
+
}
836
+
}
837
+
```
838
+
839
+
You can almost think of information flowing in a loop:
840
+
841
+

842
+
843
+
Applications write to the repo. The write events are then emitted on the firehose where they're caught by the apps and
844
+
ingested into their databases.
845
+
846
+
Why sync from the event log like this? Because there are other apps in the network that will write the records we're
847
+
interested in. By subscribing to the event log (via the Jetstream), we ensure that we catch all the data we're interested in —
848
+
including data published by other apps!
849
+
850
+
## Step 7. Listing the latest statuses
851
+
852
+
Now that we have statuses populating our SQLite, we can produce a timeline of status updates by users. We also use
853
+
a [DID](https://atproto.com/specs/did)-to-handle resolver so we can show a nice username with the statuses:
854
+
```rust
855
+
/** ./src/main.rs **/
856
+
// Homepage
857
+
/// Home
858
+
#[get("/")]
859
+
async fn home(
860
+
session: Session,
861
+
oauth_client: web::Data<OAuthClientType>,
862
+
db_pool: web::Data<Arc<Pool>>,
863
+
handle_resolver: web::Data<HandleResolver>,
864
+
) -> Result<impl Responder> {
865
+
const TITLE: &str = "Home";
866
+
// Fetch data stored in our SQLite
867
+
let mut statuses = StatusFromDb::load_latest_statuses(&db_pool)
868
+
.await
869
+
.unwrap_or_else(|err| {
870
+
log::error!("Error loading statuses: {err}");
871
+
vec![]
872
+
});
873
+
874
+
// We resolve the handles to the DID. This is a bit messy atm,
875
+
// and there are hopes to find a cleaner way
876
+
// to handle resolving the DIDs and formating the handles,
877
+
// But it gets the job done for the purpose of this tutorial.
878
+
// PRs are welcomed!
879
+
880
+
//Simple way to cut down on resolve calls if we already know the handle for the did
881
+
let mut quick_resolve_map: HashMap<Did, String> = HashMap::new();
882
+
for db_status in &mut statuses {
883
+
let authors_did = Did::new(db_status.author_did.clone()).expect("failed to parse did");
884
+
//Check to see if we already resolved it to cut down on resolve requests
885
+
match quick_resolve_map.get(&authors_did) {
886
+
None => {}
887
+
Some(found_handle) => {
888
+
db_status.handle = Some(found_handle.clone());
889
+
continue;
890
+
}
891
+
}
892
+
//Attempts to resolve the DID to a handle
893
+
db_status.handle = match handle_resolver.resolve(&authors_did).await {
894
+
Ok(did_doc) => {
895
+
match did_doc.also_known_as {
896
+
None => None,
897
+
Some(also_known_as) => {
898
+
match also_known_as.is_empty() {
899
+
true => None,
900
+
false => {
901
+
//also_known as a list starts the array with the highest priority handle
902
+
let formatted_handle =
903
+
format!("@{}", also_known_as[0]).replace("at://", "");
904
+
quick_resolve_map.insert(authors_did, formatted_handle.clone());
905
+
Some(formatted_handle)
906
+
}
907
+
}
908
+
}
909
+
}
910
+
}
911
+
Err(err) => {
912
+
log::error!("Error resolving did: {err}");
913
+
None
914
+
}
915
+
};
916
+
}
917
+
// ...
918
+
```
919
+
>[!NOTE]
920
+
> We use a newly released handle resolver from atrium.
921
+
> Can see
922
+
> how it is set up in [./src/main.rs](https://github.com/fatfingers23/rusty_statusphere_example_app/blob/a13ab7eb8fcba901a483468f7fd7c56b2948972d/src/main.rs#L508)
923
+
924
+
925
+
Our HTML can now list these status records:
926
+
927
+
```html
928
+
<!-- ./templates/home.html -->
929
+
{% for status in statuses %}
930
+
<div class="{% if loop.first %} status-line no-line {% else %} status-line {% endif %} ">
931
+
<div>
932
+
<div class="status">{{status.status}}</div>
933
+
</div>
934
+
<div class="desc">
935
+
<a class="author"
936
+
href="https://bsky.app/profile/{{status.author_did}}">{{status.author_display_name()}}</a>
937
+
{% if status.is_today() %}
938
+
is feeling {{status.status}} today
939
+
{% else %}
940
+
was feeling {{status.status}} on {{status.created_at}}
941
+
{% endif %}
942
+
</div>
943
+
</div>
944
+
{% endfor %}
945
+
`
946
+
})}
947
+
```
948
+
949
+

950
+
951
+
## Step 8. Optimistic updates
952
+
953
+
As a final optimization, let's introduce "optimistic updates."
954
+
955
+
Remember the information flow loop with the repo write and the event log?
956
+
957
+

958
+
959
+
Since we're updating our users' repos locally, we can short-circuit that flow to our own database:
960
+
961
+

962
+
963
+
This is an important optimization to make, because it ensures that the user sees their own changes while using your app.
964
+
When the event eventually arrives from the firehose, we just discard it since we already have it saved locally.
965
+
966
+
To do this, we just update `POST /status` to include an additional write to our SQLite DB:
967
+
968
+
```rust
969
+
/** ./src/main.rs **/
970
+
/// Creates a new status
971
+
#[post("/status")]
972
+
async fn status(
973
+
request: HttpRequest,
974
+
session: Session,
975
+
oauth_client: web::Data<OAuthClientType>,
976
+
db_pool: web::Data<Arc<Pool>>,
977
+
form: web::Form<StatusForm>,
978
+
) -> HttpResponse {
979
+
//...
980
+
let create_result = agent
981
+
.api
982
+
.com
983
+
.atproto
984
+
.repo
985
+
.create_record(
986
+
atrium_api::com::atproto::repo::create_record::InputData {
987
+
collection: Status::NSID.parse().unwrap(),
988
+
repo: did.into(),
989
+
rkey: None,
990
+
record: status.into(),
991
+
swap_commit: None,
992
+
validate: None,
993
+
}
994
+
.into(),
995
+
)
996
+
.await;
997
+
998
+
match create_result {
999
+
Ok(record) => {
1000
+
let status = StatusFromDb::new(
1001
+
record.uri.clone(),
1002
+
did_string,
1003
+
form.status.clone(),
1004
+
);
1005
+
1006
+
let _ = status.save(db_pool).await;
1007
+
Redirect::to("/")
1008
+
.see_other()
1009
+
.respond_to(&request)
1010
+
.map_into_boxed_body()
1011
+
}
1012
+
Err(err) => {
1013
+
log::error!("Error creating status: {err}");
1014
+
let error_html = ErrorTemplate {
1015
+
title: "Error",
1016
+
error: "Was an error creating the status, please check the logs.",
1017
+
}
1018
+
.render()
1019
+
.expect("template should be valid");
1020
+
HttpResponse::Ok().body(error_html)
1021
+
}
1022
+
}
1023
+
//...
1024
+
}
1025
+
```
1026
+
1027
+
You'll notice this code looks almost exactly like what we're doing in `ingester.rs`.
1028
+
1029
+
## Thinking in AT Proto
1030
+
1031
+
In this tutorial we've covered the key steps to building an atproto app. Data is published in its canonical form on
1032
+
users' `at://` repos and then aggregated into apps' databases to produce views of the network.
1033
+
1034
+
When building your app, think in these four key steps:
1035
+
1036
+
- Design the [Lexicon](#) schemas for the records you'll publish into the Atmosphere.
1037
+
- Create a database for aggregating the records into useful views.
1038
+
- Build your application to write the records on your users' repos.
1039
+
- Listen to the firehose to aggregate data across the network.
1040
+
1041
+
Remember this flow of information throughout:
1042
+
1043
+

1044
+
1045
+
This is how every app in the Atmosphere works, including the [Bluesky social app](https://bsky.app).
1046
+
1047
+
## Next steps
1048
+
1049
+
If you want to practice what you've learned, here are some additional challenges you could try:
1050
+
1051
+
- Sync the profile records of all users so that you can show their display names instead of their handles.
1052
+
- Count the number of each status used and display the total counts.
1053
+
- Fetch the authed user's `app.bsky.graph.follow` follows and show statuses from them.
1054
+
- Create a different kind of schema, like a way to post links to websites and rate them 1 through 4 stars.
1055
+
1056
+
[Ready to learn more? Specs, guides, and SDKs can be found here.](https://atproto.com/)
1057
+
1058
+
>[!NOTE]
1059
+
> Thank you for checking out my version of the Statusphere example project!
1060
+
> There are parts of this I feel can be improved on and made more efficient,
1061
+
> but I think it does a good job for providing you with a starting point to start building Rust applications in the Atmosphere.
1062
+
> See something you think could be done better? Then please submit a PR!
1063
+
> [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev)
images/cover.png
images/cover.png
This is a binary file and will not be displayed.
images/emojis.png
images/emojis.png
This is a binary file and will not be displayed.
+23
lexicons/status.json
+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
public/css/base.css
···
1
-
@import 'tailwindcss';
-74
src/controllers/FeedController.rs
-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
-9
src/controllers/mod.rs
+408
src/db.rs
+408
src/db.rs
···
1
+
use actix_web::web::Data;
2
+
use async_sqlite::{
3
+
Pool, rusqlite,
4
+
rusqlite::{Error, Row},
5
+
};
6
+
use atrium_api::types::string::Did;
7
+
use chrono::{DateTime, Datelike, Utc};
8
+
use rusqlite::types::Type;
9
+
use serde::{Deserialize, Serialize};
10
+
use std::{fmt::Debug, sync::Arc};
11
+
12
+
/// Creates the tables in the db.
13
+
pub async fn create_tables_in_database(pool: &Pool) -> Result<(), async_sqlite::Error> {
14
+
pool.conn(move |conn| {
15
+
conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
16
+
17
+
// status
18
+
conn.execute(
19
+
"CREATE TABLE IF NOT EXISTS status (
20
+
uri TEXT PRIMARY KEY,
21
+
authorDid TEXT NOT NULL,
22
+
status TEXT NOT NULL,
23
+
createdAt INTEGER NOT NULL,
24
+
indexedAt INTEGER NOT NULL
25
+
)",
26
+
[],
27
+
)
28
+
.unwrap();
29
+
30
+
// auth_session
31
+
conn.execute(
32
+
"CREATE TABLE IF NOT EXISTS auth_session (
33
+
key TEXT PRIMARY KEY,
34
+
session TEXT NOT NULL
35
+
)",
36
+
[],
37
+
)
38
+
.unwrap();
39
+
40
+
// auth_state
41
+
conn.execute(
42
+
"CREATE TABLE IF NOT EXISTS auth_state (
43
+
key TEXT PRIMARY KEY,
44
+
state TEXT NOT NULL
45
+
)",
46
+
[],
47
+
)
48
+
.unwrap();
49
+
Ok(())
50
+
})
51
+
.await?;
52
+
Ok(())
53
+
}
54
+
55
+
///Status table datatype
56
+
#[derive(Debug, Clone, Deserialize, Serialize)]
57
+
pub struct StatusFromDb {
58
+
pub uri: String,
59
+
pub author_did: String,
60
+
pub status: String,
61
+
pub created_at: DateTime<Utc>,
62
+
pub indexed_at: DateTime<Utc>,
63
+
pub handle: Option<String>,
64
+
}
65
+
66
+
//Status methods
67
+
impl StatusFromDb {
68
+
/// Creates a new [StatusFromDb]
69
+
pub fn new(uri: String, author_did: String, status: String) -> Self {
70
+
let now = chrono::Utc::now();
71
+
Self {
72
+
uri,
73
+
author_did,
74
+
status,
75
+
created_at: now,
76
+
indexed_at: now,
77
+
handle: None,
78
+
}
79
+
}
80
+
81
+
/// Helper to map from [Row] to [StatusDb]
82
+
fn map_from_row(row: &Row) -> Result<Self, rusqlite::Error> {
83
+
Ok(Self {
84
+
uri: row.get(0)?,
85
+
author_did: row.get(1)?,
86
+
status: row.get(2)?,
87
+
//DateTimes are stored as INTEGERS then parsed into a DateTime<UTC>
88
+
created_at: {
89
+
let timestamp: i64 = row.get(3)?;
90
+
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| {
91
+
Error::InvalidColumnType(3, "Invalid timestamp".parse().unwrap(), Type::Text)
92
+
})?
93
+
},
94
+
//DateTimes are stored as INTEGERS then parsed into a DateTime<UTC>
95
+
indexed_at: {
96
+
let timestamp: i64 = row.get(4)?;
97
+
DateTime::from_timestamp(timestamp, 0).ok_or_else(|| {
98
+
Error::InvalidColumnType(4, "Invalid timestamp".parse().unwrap(), Type::Text)
99
+
})?
100
+
},
101
+
handle: None,
102
+
})
103
+
}
104
+
105
+
/// Helper for the UI to see if indexed_at date is today or not
106
+
pub fn is_today(&self) -> bool {
107
+
let now = Utc::now();
108
+
109
+
self.indexed_at.day() == now.day()
110
+
&& self.indexed_at.month() == now.month()
111
+
&& self.indexed_at.year() == now.year()
112
+
}
113
+
114
+
/// Saves the [StatusDb]
115
+
pub async fn save(&self, pool: Data<Arc<Pool>>) -> Result<(), async_sqlite::Error> {
116
+
let cloned_self = self.clone();
117
+
pool.conn(move |conn| {
118
+
Ok(conn.execute(
119
+
"INSERT INTO status (uri, authorDid, status, createdAt, indexedAt) VALUES (?1, ?2, ?3, ?4, ?5)",
120
+
[
121
+
&cloned_self.uri,
122
+
&cloned_self.author_did,
123
+
&cloned_self.status,
124
+
&cloned_self.created_at.timestamp().to_string(),
125
+
&cloned_self.indexed_at.timestamp().to_string(),
126
+
],
127
+
)?)
128
+
})
129
+
.await?;
130
+
Ok(())
131
+
}
132
+
133
+
/// Saves or updates a status by its did(uri)
134
+
pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> {
135
+
let cloned_self = self.clone();
136
+
pool.conn(move |conn| {
137
+
//We check to see if the session already exists, if so we need to update not insert
138
+
let mut stmt = conn.prepare("SELECT COUNT(*) FROM status WHERE uri = ?1")?;
139
+
let count: i64 = stmt.query_row([&cloned_self.uri], |row| row.get(0))?;
140
+
match count > 0 {
141
+
true => {
142
+
let mut update_stmt =
143
+
conn.prepare("UPDATE status SET status = ?2, indexedAt = ? WHERE uri = ?1")?;
144
+
update_stmt.execute([&cloned_self.uri, &cloned_self.status, &cloned_self.indexed_at.timestamp().to_string()])?;
145
+
Ok(())
146
+
}
147
+
false => {
148
+
conn.execute(
149
+
"INSERT INTO status (uri, authorDid, status, createdAt, indexedAt) VALUES (?1, ?2, ?3, ?4, ?5)",
150
+
[
151
+
&cloned_self.uri,
152
+
&cloned_self.author_did,
153
+
&cloned_self.status,
154
+
&cloned_self.created_at.timestamp().to_string(),
155
+
&cloned_self.indexed_at.timestamp().to_string(),
156
+
],
157
+
)?;
158
+
Ok(())
159
+
}
160
+
}
161
+
})
162
+
.await?;
163
+
Ok(())
164
+
}
165
+
pub async fn delete_by_uri(pool: &Pool, uri: String) -> Result<(), async_sqlite::Error> {
166
+
pool.conn(move |conn| {
167
+
let mut stmt = conn.prepare("DELETE FROM status WHERE uri = ?1")?;
168
+
stmt.execute([&uri])
169
+
})
170
+
.await?;
171
+
Ok(())
172
+
}
173
+
174
+
/// Loads the last 10 statuses we have saved
175
+
pub async fn load_latest_statuses(
176
+
pool: &Data<Arc<Pool>>,
177
+
) -> Result<Vec<Self>, async_sqlite::Error> {
178
+
Ok(pool
179
+
.conn(move |conn| {
180
+
let mut stmt =
181
+
conn.prepare("SELECT * FROM status ORDER BY indexedAt DESC LIMIT 10")?;
182
+
let status_iter = stmt
183
+
.query_map([], |row| Ok(Self::map_from_row(row).unwrap()))
184
+
.unwrap();
185
+
186
+
let mut statuses = Vec::new();
187
+
for status in status_iter {
188
+
statuses.push(status?);
189
+
}
190
+
Ok(statuses)
191
+
})
192
+
.await?)
193
+
}
194
+
195
+
/// Loads the logged-in users current status
196
+
pub async fn my_status(
197
+
pool: &Data<Arc<Pool>>,
198
+
did: &str,
199
+
) -> Result<Option<Self>, async_sqlite::Error> {
200
+
let did = did.to_string();
201
+
pool.conn(move |conn| {
202
+
let mut stmt = conn.prepare(
203
+
"SELECT * FROM status WHERE authorDid = ?1 ORDER BY createdAt DESC LIMIT 1",
204
+
)?;
205
+
stmt.query_row([did.as_str()], |row| Self::map_from_row(row))
206
+
.map(Some)
207
+
.or_else(|err| {
208
+
if err == rusqlite::Error::QueryReturnedNoRows {
209
+
Ok(None)
210
+
} else {
211
+
Err(err)
212
+
}
213
+
})
214
+
})
215
+
.await
216
+
}
217
+
218
+
/// ui helper to show a handle or did if the handle cannot be found
219
+
pub fn author_display_name(&self) -> String {
220
+
match self.handle.as_ref() {
221
+
Some(handle) => handle.to_string(),
222
+
None => self.author_did.to_string(),
223
+
}
224
+
}
225
+
}
226
+
227
+
/// AuthSession table data type
228
+
#[derive(Debug, Clone, Deserialize, Serialize)]
229
+
pub struct AuthSession {
230
+
pub key: String,
231
+
pub session: String,
232
+
}
233
+
234
+
impl AuthSession {
235
+
/// Creates a new [AuthSession]
236
+
pub fn new<V>(key: String, session: V) -> Self
237
+
where
238
+
V: Serialize,
239
+
{
240
+
let session = serde_json::to_string(&session).unwrap();
241
+
Self {
242
+
key: key.to_string(),
243
+
session,
244
+
}
245
+
}
246
+
247
+
/// Helper to map from [Row] to [AuthSession]
248
+
fn map_from_row(row: &Row) -> Result<Self, Error> {
249
+
let key: String = row.get(0)?;
250
+
let session: String = row.get(1)?;
251
+
Ok(Self { key, session })
252
+
}
253
+
254
+
/// Gets a session by the users did(key)
255
+
pub async fn get_by_did(pool: &Pool, did: String) -> Result<Option<Self>, async_sqlite::Error> {
256
+
let did = Did::new(did).unwrap();
257
+
pool.conn(move |conn| {
258
+
let mut stmt = conn.prepare("SELECT * FROM auth_session WHERE key = ?1")?;
259
+
stmt.query_row([did.as_str()], |row| Self::map_from_row(row))
260
+
.map(Some)
261
+
.or_else(|err| {
262
+
if err == Error::QueryReturnedNoRows {
263
+
Ok(None)
264
+
} else {
265
+
Err(err)
266
+
}
267
+
})
268
+
})
269
+
.await
270
+
}
271
+
272
+
/// Saves or updates the session by its did(key)
273
+
pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> {
274
+
let cloned_self = self.clone();
275
+
pool.conn(move |conn| {
276
+
//We check to see if the session already exists, if so we need to update not insert
277
+
let mut stmt = conn.prepare("SELECT COUNT(*) FROM auth_session WHERE key = ?1")?;
278
+
let count: i64 = stmt.query_row([&cloned_self.key], |row| row.get(0))?;
279
+
match count > 0 {
280
+
true => {
281
+
let mut update_stmt =
282
+
conn.prepare("UPDATE auth_session SET session = ?2 WHERE key = ?1")?;
283
+
update_stmt.execute([&cloned_self.key, &cloned_self.session])?;
284
+
Ok(())
285
+
}
286
+
false => {
287
+
conn.execute(
288
+
"INSERT INTO auth_session (key, session) VALUES (?1, ?2)",
289
+
[&cloned_self.key, &cloned_self.session],
290
+
)?;
291
+
Ok(())
292
+
}
293
+
}
294
+
})
295
+
.await?;
296
+
Ok(())
297
+
}
298
+
299
+
/// Deletes the session by did
300
+
pub async fn delete_by_did(pool: &Pool, did: String) -> Result<(), async_sqlite::Error> {
301
+
pool.conn(move |conn| {
302
+
let mut stmt = conn.prepare("DELETE FROM auth_session WHERE key = ?1")?;
303
+
stmt.execute([&did])
304
+
})
305
+
.await?;
306
+
Ok(())
307
+
}
308
+
309
+
/// Deletes all the sessions
310
+
pub async fn delete_all(pool: &Pool) -> Result<(), async_sqlite::Error> {
311
+
pool.conn(move |conn| {
312
+
let mut stmt = conn.prepare("DELETE FROM auth_session")?;
313
+
stmt.execute([])
314
+
})
315
+
.await?;
316
+
Ok(())
317
+
}
318
+
}
319
+
320
+
/// AuthState table datatype
321
+
#[derive(Debug, Clone, Deserialize, Serialize)]
322
+
pub struct AuthState {
323
+
pub key: String,
324
+
pub state: String,
325
+
}
326
+
327
+
impl AuthState {
328
+
/// Creates a new [AuthState]
329
+
pub fn new<V>(key: String, state: V) -> Self
330
+
where
331
+
V: Serialize,
332
+
{
333
+
let state = serde_json::to_string(&state).unwrap();
334
+
Self {
335
+
key: key.to_string(),
336
+
state,
337
+
}
338
+
}
339
+
340
+
/// Helper to map from [Row] to [AuthState]
341
+
fn map_from_row(row: &Row) -> Result<Self, Error> {
342
+
let key: String = row.get(0)?;
343
+
let state: String = row.get(1)?;
344
+
Ok(Self { key, state })
345
+
}
346
+
347
+
/// Gets a state by the users key
348
+
pub async fn get_by_key(pool: &Pool, key: String) -> Result<Option<Self>, async_sqlite::Error> {
349
+
pool.conn(move |conn| {
350
+
let mut stmt = conn.prepare("SELECT * FROM auth_state WHERE key = ?1")?;
351
+
stmt.query_row([key.as_str()], |row| Self::map_from_row(row))
352
+
.map(Some)
353
+
.or_else(|err| {
354
+
if err == Error::QueryReturnedNoRows {
355
+
Ok(None)
356
+
} else {
357
+
Err(err)
358
+
}
359
+
})
360
+
})
361
+
.await
362
+
}
363
+
364
+
/// Saves or updates the state by its key
365
+
pub async fn save_or_update(&self, pool: &Pool) -> Result<(), async_sqlite::Error> {
366
+
let cloned_self = self.clone();
367
+
pool.conn(move |conn| {
368
+
//We check to see if the state already exists, if so we need to update
369
+
let mut stmt = conn.prepare("SELECT COUNT(*) FROM auth_state WHERE key = ?1")?;
370
+
let count: i64 = stmt.query_row([&cloned_self.key], |row| row.get(0))?;
371
+
match count > 0 {
372
+
true => {
373
+
let mut update_stmt =
374
+
conn.prepare("UPDATE auth_state SET state = ?2 WHERE key = ?1")?;
375
+
update_stmt.execute([&cloned_self.key, &cloned_self.state])?;
376
+
Ok(())
377
+
}
378
+
false => {
379
+
conn.execute(
380
+
"INSERT INTO auth_state (key, state) VALUES (?1, ?2)",
381
+
[&cloned_self.key, &cloned_self.state],
382
+
)?;
383
+
Ok(())
384
+
}
385
+
}
386
+
})
387
+
.await?;
388
+
Ok(())
389
+
}
390
+
391
+
pub async fn delete_by_key(pool: &Pool, key: String) -> Result<(), async_sqlite::Error> {
392
+
pool.conn(move |conn| {
393
+
let mut stmt = conn.prepare("DELETE FROM auth_state WHERE key = ?1")?;
394
+
stmt.execute([&key])
395
+
})
396
+
.await?;
397
+
Ok(())
398
+
}
399
+
400
+
pub async fn delete_all(pool: &Pool) -> Result<(), async_sqlite::Error> {
401
+
pool.conn(move |conn| {
402
+
let mut stmt = conn.prepare("DELETE FROM auth_state")?;
403
+
stmt.execute([])
404
+
})
405
+
.await?;
406
+
Ok(())
407
+
}
408
+
}
+114
src/ingester.rs
+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
+3
src/lexicons/mod.rs
+23
src/lexicons/record.rs
+23
src/lexicons/record.rs
···
1
+
// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2
+
//!A collection of known record types.
3
+
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
4
+
#[serde(tag = "$type")]
5
+
pub enum KnownRecord {
6
+
#[serde(rename = "xyz.statusphere.status")]
7
+
LexiconsXyzStatusphereStatus(Box<crate::lexicons::xyz::statusphere::status::Record>),
8
+
}
9
+
impl From<crate::lexicons::xyz::statusphere::status::Record> for KnownRecord {
10
+
fn from(record: crate::lexicons::xyz::statusphere::status::Record) -> Self {
11
+
KnownRecord::LexiconsXyzStatusphereStatus(Box::new(record))
12
+
}
13
+
}
14
+
impl From<crate::lexicons::xyz::statusphere::status::RecordData> for KnownRecord {
15
+
fn from(record_data: crate::lexicons::xyz::statusphere::status::RecordData) -> Self {
16
+
KnownRecord::LexiconsXyzStatusphereStatus(Box::new(record_data.into()))
17
+
}
18
+
}
19
+
impl Into<atrium_api::types::Unknown> for KnownRecord {
20
+
fn into(self) -> atrium_api::types::Unknown {
21
+
atrium_api::types::TryIntoUnknown::try_into_unknown(&self).unwrap()
22
+
}
23
+
}
+15
src/lexicons/xyz/statusphere/status.rs
+15
src/lexicons/xyz/statusphere/status.rs
···
1
+
// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2
+
//!Definitions for the `xyz.statusphere.status` namespace.
3
+
use atrium_api::types::TryFromUnknown;
4
+
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
5
+
#[serde(rename_all = "camelCase")]
6
+
pub struct RecordData {
7
+
pub created_at: atrium_api::types::string::Datetime,
8
+
pub status: String,
9
+
}
10
+
pub type Record = atrium_api::types::Object<RecordData>;
11
+
impl From<atrium_api::types::Unknown> for RecordData {
12
+
fn from(value: atrium_api::types::Unknown) -> Self {
13
+
Self::try_from_unknown(value).unwrap()
14
+
}
15
+
}
+9
src/lexicons/xyz/statusphere.rs
+9
src/lexicons/xyz/statusphere.rs
···
1
+
// @generated - This file is generated by esquema-codegen (forked from atrium-codegen). DO NOT EDIT.
2
+
//!Definitions for the `xyz.statusphere` namespace.
3
+
pub mod status;
4
+
#[derive(Debug)]
5
+
pub struct Status;
6
+
impl atrium_api::types::Collection for Status {
7
+
const NSID: &'static str = "xyz.statusphere.status";
8
+
type Record = status::Record;
9
+
}
+3
src/lexicons/xyz.rs
+3
src/lexicons/xyz.rs
+562
-7
src/main.rs
+562
-7
src/main.rs
···
1
+
use crate::{
2
+
db::{StatusFromDb, create_tables_in_database},
3
+
ingester::start_ingester,
4
+
lexicons::record::KnownRecord,
5
+
lexicons::xyz::statusphere::Status,
6
+
storage::{SqliteSessionStore, SqliteStateStore},
7
+
templates::{HomeTemplate, LoginTemplate},
8
+
};
1
9
use actix_files::Files;
2
-
use actix_web::{App, HttpServer, Responder, Result, middleware, web};
3
-
use controllers::FeedController::feed_controller;
4
-
use std::collections::HashMap;
10
+
use actix_session::{
11
+
Session, SessionMiddleware, config::PersistentSession, storage::CookieSessionStore,
12
+
};
13
+
use actix_web::{
14
+
App, HttpRequest, HttpResponse, HttpServer, Responder, Result,
15
+
cookie::{self, Key},
16
+
get, middleware, post,
17
+
web::{self, Redirect},
18
+
};
19
+
use askama::Template;
20
+
use async_sqlite::{Pool, PoolBuilder};
21
+
use atrium_api::{
22
+
agent::Agent,
23
+
types::Collection,
24
+
types::string::{Datetime, Did},
25
+
};
26
+
use atrium_common::resolver::Resolver;
27
+
use atrium_identity::{
28
+
did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL},
29
+
handle::{AtprotoHandleResolver, AtprotoHandleResolverConfig},
30
+
};
31
+
use atrium_oauth::{
32
+
AtprotoLocalhostClientMetadata, AuthorizeOptions, CallbackParams, DefaultHttpClient,
33
+
KnownScope, OAuthClient, OAuthClientConfig, OAuthResolverConfig, Scope,
34
+
};
35
+
use dotenv::dotenv;
36
+
use resolver::HickoryDnsTxtResolver;
37
+
use serde::{Deserialize, Serialize};
38
+
use std::{
39
+
collections::HashMap,
40
+
io::{Error, ErrorKind},
41
+
sync::Arc,
42
+
};
43
+
use templates::{ErrorTemplate, Profile};
44
+
45
+
extern crate dotenv;
46
+
47
+
mod db;
48
+
mod ingester;
49
+
mod lexicons;
50
+
mod resolver;
51
+
mod storage;
52
+
mod templates;
53
+
54
+
/// OAuthClientType to make it easier to access the OAuthClient in web requests
55
+
type OAuthClientType = Arc<
56
+
OAuthClient<
57
+
SqliteStateStore,
58
+
SqliteSessionStore,
59
+
CommonDidResolver<DefaultHttpClient>,
60
+
AtprotoHandleResolver<HickoryDnsTxtResolver, DefaultHttpClient>,
61
+
>,
62
+
>;
63
+
64
+
/// HandleResolver to make it easier to access the OAuthClient in web requests
65
+
type HandleResolver = Arc<CommonDidResolver<DefaultHttpClient>>;
66
+
67
+
/// All the available emoji status options
68
+
const STATUS_OPTIONS: [&str; 29] = [
69
+
"๐",
70
+
"๐",
71
+
"๐",
72
+
"๐ฅน",
73
+
"๐ง",
74
+
"๐ค",
75
+
"๐",
76
+
"๐",
77
+
"๐",
78
+
"๐ค",
79
+
"๐คจ",
80
+
"๐ฅณ",
81
+
"๐ญ",
82
+
"๐ค",
83
+
"๐คฏ",
84
+
"๐ซก",
85
+
"๐",
86
+
"โ",
87
+
"๐ค",
88
+
"๐",
89
+
"๐ง ",
90
+
"๐ฉโ๐ป",
91
+
"๐งโ๐ป",
92
+
"๐ฅท",
93
+
"๐ง",
94
+
"๐ฆ",
95
+
"๐",
96
+
"๐ฅ",
97
+
"๐ฆ",
98
+
];
99
+
100
+
/// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L71
101
+
/// OAuth callback endpoint to complete session creation
102
+
#[get("/oauth/callback")]
103
+
async fn oauth_callback(
104
+
request: HttpRequest,
105
+
params: web::Query<CallbackParams>,
106
+
oauth_client: web::Data<OAuthClientType>,
107
+
session: Session,
108
+
) -> HttpResponse {
109
+
//Processes the call back and parses out a session if found and valid
110
+
match oauth_client.callback(params.into_inner()).await {
111
+
Ok((bsky_session, _)) => {
112
+
let agent = Agent::new(bsky_session);
113
+
match agent.did().await {
114
+
Some(did) => {
115
+
session.insert("did", did).unwrap();
116
+
Redirect::to("/")
117
+
.see_other()
118
+
.respond_to(&request)
119
+
.map_into_boxed_body()
120
+
}
121
+
None => {
122
+
let html = ErrorTemplate {
123
+
title: "Error",
124
+
error: "The OAuth agent did not return a DID. May try re-logging in.",
125
+
};
126
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
127
+
}
128
+
}
129
+
}
130
+
Err(err) => {
131
+
log::error!("Error: {err}");
132
+
let html = ErrorTemplate {
133
+
title: "Error",
134
+
error: "OAuth error, check the logs",
135
+
};
136
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
137
+
}
138
+
}
139
+
}
140
+
141
+
/// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L93
142
+
/// Takes you to the login page
143
+
#[get("/login")]
144
+
async fn login() -> Result<impl Responder> {
145
+
let html = LoginTemplate {
146
+
title: "Log in",
147
+
error: None,
148
+
};
149
+
Ok(web::Html::new(
150
+
html.render().expect("template should be valid"),
151
+
))
152
+
}
153
+
154
+
/// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L93
155
+
/// Logs you out by destroying your cookie on the server and web browser
156
+
#[get("/logout")]
157
+
async fn logout(request: HttpRequest, session: Session) -> HttpResponse {
158
+
session.purge();
159
+
Redirect::to("/")
160
+
.see_other()
161
+
.respond_to(&request)
162
+
.map_into_boxed_body()
163
+
}
164
+
165
+
/// The post body for logging in
166
+
#[derive(Serialize, Deserialize, Clone)]
167
+
struct LoginForm {
168
+
handle: String,
169
+
}
170
+
171
+
/// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L101
172
+
/// Login endpoint
173
+
#[post("/login")]
174
+
async fn login_post(
175
+
request: HttpRequest,
176
+
params: web::Form<LoginForm>,
177
+
oauth_client: web::Data<OAuthClientType>,
178
+
) -> HttpResponse {
179
+
// This will act the same as the js method isValidHandle to make sure it is valid
180
+
match atrium_api::types::string::Handle::new(params.handle.clone()) {
181
+
Ok(handle) => {
182
+
//Creates the oauth url to redirect to for the user to log in with their credentials
183
+
let oauth_url = oauth_client
184
+
.authorize(
185
+
&handle,
186
+
AuthorizeOptions {
187
+
scopes: vec![
188
+
Scope::Known(KnownScope::Atproto),
189
+
Scope::Known(KnownScope::TransitionGeneric),
190
+
],
191
+
..Default::default()
192
+
},
193
+
)
194
+
.await;
195
+
match oauth_url {
196
+
Ok(url) => Redirect::to(url)
197
+
.see_other()
198
+
.respond_to(&request)
199
+
.map_into_boxed_body(),
200
+
Err(err) => {
201
+
log::error!("Error: {err}");
202
+
let html = LoginTemplate {
203
+
title: "Log in",
204
+
error: Some("OAuth error"),
205
+
};
206
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
207
+
}
208
+
}
209
+
}
210
+
Err(err) => {
211
+
let html: LoginTemplate<'_> = LoginTemplate {
212
+
title: "Log in",
213
+
error: Some(err),
214
+
};
215
+
HttpResponse::Ok().body(html.render().expect("template should be valid"))
216
+
}
217
+
}
218
+
}
219
+
220
+
/// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L146
221
+
/// Home
222
+
#[get("/")]
223
+
async fn home(
224
+
session: Session,
225
+
oauth_client: web::Data<OAuthClientType>,
226
+
db_pool: web::Data<Arc<Pool>>,
227
+
handle_resolver: web::Data<HandleResolver>,
228
+
) -> Result<impl Responder> {
229
+
const TITLE: &str = "Home";
230
+
//Loads the last 10 statuses saved in the DB
231
+
let mut statuses = StatusFromDb::load_latest_statuses(&db_pool)
232
+
.await
233
+
.unwrap_or_else(|err| {
234
+
log::error!("Error loading statuses: {err}");
235
+
vec![]
236
+
});
237
+
238
+
//Simple way to cut down on resolve calls if we already know the handle for the did
239
+
let mut quick_resolve_map: HashMap<Did, String> = HashMap::new();
240
+
// We resolve the handles to the DID. This is a bit messy atm,
241
+
// and there are hopes to find a cleaner way
242
+
// to handle resolving the DIDs and formating the handles,
243
+
// But it gets the job done for the purpose of this tutorial.
244
+
// PRs are welcomed!
245
+
for db_status in &mut statuses {
246
+
let authors_did = Did::new(db_status.author_did.clone()).expect("failed to parse did");
247
+
//Check to see if we already resolved it to cut down on resolve requests
248
+
match quick_resolve_map.get(&authors_did) {
249
+
None => {}
250
+
Some(found_handle) => {
251
+
db_status.handle = Some(found_handle.clone());
252
+
continue;
253
+
}
254
+
}
255
+
//Attempts to resolve the DID to a handle
256
+
db_status.handle = match handle_resolver.resolve(&authors_did).await {
257
+
Ok(did_doc) => {
258
+
match did_doc.also_known_as {
259
+
None => None,
260
+
Some(also_known_as) => {
261
+
match also_known_as.is_empty() {
262
+
true => None,
263
+
false => {
264
+
//also_known as a list starts the array with the highest priority handle
265
+
let formatted_handle =
266
+
format!("@{}", also_known_as[0]).replace("at://", "");
267
+
quick_resolve_map.insert(authors_did, formatted_handle.clone());
268
+
Some(formatted_handle)
269
+
}
270
+
}
271
+
}
272
+
}
273
+
}
274
+
Err(err) => {
275
+
log::error!("Error resolving did: {err}");
276
+
None
277
+
}
278
+
};
279
+
}
280
+
281
+
// If the user is signed in, get an agent which communicates with their server
282
+
match session.get::<String>("did").unwrap_or(None) {
283
+
Some(did) => {
284
+
let did = Did::new(did).expect("failed to parse did");
285
+
//Grabs the users last status to highlight it in the ui
286
+
let my_status = StatusFromDb::my_status(&db_pool, &did)
287
+
.await
288
+
.unwrap_or_else(|err| {
289
+
log::error!("Error loading my status: {err}");
290
+
None
291
+
});
292
+
293
+
// gets the user's session from the session store to resume
294
+
match oauth_client.restore(&did).await {
295
+
Ok(session) => {
296
+
//Creates an agent to make authenticated requests
297
+
let agent = Agent::new(session);
298
+
299
+
// Fetch additional information about the logged-in user
300
+
let profile = agent
301
+
.api
302
+
.app
303
+
.bsky
304
+
.actor
305
+
.get_profile(
306
+
atrium_api::app::bsky::actor::get_profile::ParametersData {
307
+
actor: atrium_api::types::string::AtIdentifier::Did(did),
308
+
}
309
+
.into(),
310
+
)
311
+
.await;
312
+
313
+
let html = HomeTemplate {
314
+
title: TITLE,
315
+
status_options: &STATUS_OPTIONS,
316
+
profile: match profile {
317
+
Ok(profile) => {
318
+
let profile_data = Profile {
319
+
did: profile.did.to_string(),
320
+
display_name: profile.display_name.clone(),
321
+
};
322
+
Some(profile_data)
323
+
}
324
+
Err(err) => {
325
+
log::error!("Error accessing profile: {err}");
326
+
None
327
+
}
328
+
},
329
+
statuses,
330
+
my_status: my_status.as_ref().map(|s| s.status.clone()),
331
+
}
332
+
.render()
333
+
.expect("template should be valid");
5
334
6
-
pub mod controllers;
335
+
Ok(web::Html::new(html))
336
+
}
337
+
Err(err) => {
338
+
// Destroys the system or you're in a loop
339
+
session.purge();
340
+
log::error!("Error restoring session: {err}");
341
+
let error_html = ErrorTemplate {
342
+
title: "Error",
343
+
error: "Was an error resuming the session, please check the logs.",
344
+
}
345
+
.render()
346
+
.expect("template should be valid");
347
+
Ok(web::Html::new(error_html))
348
+
}
349
+
}
350
+
}
351
+
352
+
None => {
353
+
let html = HomeTemplate {
354
+
title: TITLE,
355
+
status_options: &STATUS_OPTIONS,
356
+
profile: None,
357
+
statuses,
358
+
my_status: None,
359
+
}
360
+
.render()
361
+
.expect("template should be valid");
362
+
363
+
Ok(web::Html::new(html))
364
+
}
365
+
}
366
+
}
367
+
368
+
/// The post body for changing your status
369
+
#[derive(Serialize, Deserialize, Clone)]
370
+
struct StatusForm {
371
+
status: String,
372
+
}
373
+
374
+
/// TS version https://github.com/bluesky-social/statusphere-example-app/blob/e4721616df50cd317c198f4c00a4818d5626d4ce/src/routes.ts#L208
375
+
/// Creates a new status
376
+
#[post("/status")]
377
+
async fn status(
378
+
request: HttpRequest,
379
+
session: Session,
380
+
oauth_client: web::Data<OAuthClientType>,
381
+
db_pool: web::Data<Arc<Pool>>,
382
+
form: web::Form<StatusForm>,
383
+
) -> HttpResponse {
384
+
// Check if the user is logged in
385
+
match session.get::<String>("did").unwrap_or(None) {
386
+
Some(did_string) => {
387
+
let did = Did::new(did_string.clone()).expect("failed to parse did");
388
+
// gets the user's session from the session store to resume
389
+
match oauth_client.restore(&did).await {
390
+
Ok(session) => {
391
+
let agent = Agent::new(session);
392
+
//Creates a strongly typed ATProto record
393
+
let status: KnownRecord = lexicons::xyz::statusphere::status::RecordData {
394
+
created_at: Datetime::now(),
395
+
status: form.status.clone(),
396
+
}
397
+
.into();
398
+
399
+
// TODO no data validation yet from esquema
400
+
// Maybe you'd like to add it? https://github.com/fatfingers23/esquema/issues/3
401
+
402
+
let create_result = agent
403
+
.api
404
+
.com
405
+
.atproto
406
+
.repo
407
+
.create_record(
408
+
atrium_api::com::atproto::repo::create_record::InputData {
409
+
collection: Status::NSID.parse().unwrap(),
410
+
repo: did.into(),
411
+
rkey: None,
412
+
record: status.into(),
413
+
swap_commit: None,
414
+
validate: None,
415
+
}
416
+
.into(),
417
+
)
418
+
.await;
419
+
420
+
match create_result {
421
+
Ok(record) => {
422
+
let status = StatusFromDb::new(
423
+
record.uri.clone(),
424
+
did_string,
425
+
form.status.clone(),
426
+
);
427
+
428
+
let _ = status.save(db_pool).await;
429
+
Redirect::to("/")
430
+
.see_other()
431
+
.respond_to(&request)
432
+
.map_into_boxed_body()
433
+
}
434
+
Err(err) => {
435
+
log::error!("Error creating status: {err}");
436
+
let error_html = ErrorTemplate {
437
+
title: "Error",
438
+
error: "Was an error creating the status, please check the logs.",
439
+
}
440
+
.render()
441
+
.expect("template should be valid");
442
+
HttpResponse::Ok().body(error_html)
443
+
}
444
+
}
445
+
}
446
+
Err(err) => {
447
+
// Destroys the system or you're in a loop
448
+
session.purge();
449
+
log::error!(
450
+
"Error restoring session, we are removing the session from the cookie: {err}"
451
+
);
452
+
let error_html = ErrorTemplate {
453
+
title: "Error",
454
+
error: "Was an error resuming the session, please check the logs.",
455
+
}
456
+
.render()
457
+
.expect("template should be valid");
458
+
HttpResponse::Ok().body(error_html)
459
+
}
460
+
}
461
+
}
462
+
None => {
463
+
let error_template = ErrorTemplate {
464
+
title: "Error",
465
+
error: "You must be logged in to create a status.",
466
+
}
467
+
.render()
468
+
.expect("template should be valid");
469
+
HttpResponse::Ok().body(error_template)
470
+
}
471
+
}
472
+
}
7
473
8
474
#[actix_web::main]
9
475
async fn main() -> std::io::Result<()> {
476
+
dotenv().ok();
10
477
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
478
+
let host = std::env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string());
479
+
let port = std::env::var("PORT")
480
+
.unwrap_or_else(|_| "8080".to_string())
481
+
.parse::<u16>()
482
+
.unwrap_or(8080);
11
483
12
-
log::info!("starting HTTP server at http://localhost:8080");
484
+
//Uses a default sqlite db path or use the one from env
485
+
let db_connection_string =
486
+
std::env::var("DB_PATH").unwrap_or_else(|_| String::from("./statusphere.sqlite3"));
487
+
488
+
//Crates a db pool to share resources to the db
489
+
let pool = match PoolBuilder::new().path(db_connection_string).open().await {
490
+
Ok(pool) => pool,
491
+
Err(err) => {
492
+
log::error!("Error creating the sqlite pool: {}", err);
493
+
return Err(Error::new(
494
+
ErrorKind::Other,
495
+
"sqlite pool could not be created.",
496
+
));
497
+
}
498
+
};
13
499
500
+
//Creates the DB and tables
501
+
create_tables_in_database(&pool)
502
+
.await
503
+
.expect("Could not create the database");
504
+
505
+
//Create a new handle resolver for the home page
506
+
let http_client = Arc::new(DefaultHttpClient::default());
507
+
508
+
let handle_resolver = CommonDidResolver::new(CommonDidResolverConfig {
509
+
plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(),
510
+
http_client: http_client.clone(),
511
+
});
512
+
let handle_resolver = Arc::new(handle_resolver);
513
+
514
+
// Create a new OAuth client
515
+
let http_client = Arc::new(DefaultHttpClient::default());
516
+
let config = OAuthClientConfig {
517
+
client_metadata: AtprotoLocalhostClientMetadata {
518
+
redirect_uris: Some(vec![String::from(format!(
519
+
//This must match the endpoint you use the callback function
520
+
"http://{host}:{port}/oauth/callback"
521
+
))]),
522
+
scopes: Some(vec![
523
+
Scope::Known(KnownScope::Atproto),
524
+
Scope::Known(KnownScope::TransitionGeneric),
525
+
]),
526
+
},
527
+
keys: None,
528
+
resolver: OAuthResolverConfig {
529
+
did_resolver: CommonDidResolver::new(CommonDidResolverConfig {
530
+
plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(),
531
+
http_client: http_client.clone(),
532
+
}),
533
+
handle_resolver: AtprotoHandleResolver::new(AtprotoHandleResolverConfig {
534
+
dns_txt_resolver: HickoryDnsTxtResolver::default(),
535
+
http_client: http_client.clone(),
536
+
}),
537
+
authorization_server_metadata: Default::default(),
538
+
protected_resource_metadata: Default::default(),
539
+
},
540
+
state_store: SqliteStateStore::new(pool.clone()),
541
+
session_store: SqliteSessionStore::new(pool.clone()),
542
+
};
543
+
let client = Arc::new(OAuthClient::new(config).expect("failed to create OAuth client"));
544
+
let arc_pool = Arc::new(pool.clone());
545
+
//Spawns the ingester that listens for other's Statusphere updates
546
+
tokio::spawn(async move {
547
+
start_ingester(arc_pool).await;
548
+
});
549
+
let arc_pool = Arc::new(pool.clone());
550
+
log::info!("starting HTTP server at http://{host}:{port}");
14
551
HttpServer::new(move || {
15
552
App::new()
16
553
.wrap(middleware::Logger::default())
554
+
.app_data(web::Data::new(client.clone()))
555
+
.app_data(web::Data::new(arc_pool.clone()))
556
+
.app_data(web::Data::new(handle_resolver.clone()))
557
+
.wrap(
558
+
SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64]))
559
+
//TODO will need to set to true in production
560
+
.cookie_secure(false)
561
+
// customize session and cookie expiration
562
+
.session_lifecycle(
563
+
PersistentSession::default().session_ttl(cookie::time::Duration::days(14)),
564
+
)
565
+
.build(),
566
+
)
17
567
.service(Files::new("/css", "public/css").show_files_listing())
18
-
.service(feed_controller())
568
+
.service(oauth_callback)
569
+
.service(login)
570
+
.service(login_post)
571
+
.service(logout)
572
+
.service(home)
573
+
.service(status)
19
574
})
20
-
.bind(("127.0.0.1", 8080))?
575
+
.bind(("127.0.0.1", port))?
21
576
.run()
22
577
.await
23
578
}
+32
src/resolver.rs
+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
+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
+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
-1
templates/base.html
+10
templates/error.html
+10
templates/error.html
-18
templates/feed.html
-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
+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
+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%}