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

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

106
+
107
+
When the user logs in, the OAuth client will create a new session with their repo server and give us read/write access
108
+
along with basic user info.
109
+
110
+

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

353
+
354
+
```html
355
+
<!-- templates/home.html -->
356
+
<div class="card">
357
+
{% if let Some(Profile {did, display_name}) = profile %}
358
+
<form action="/logout" method="post" class="session-form">
359
+
<div>
360
+
Hi,
361
+
{% if let Some(display_name) = display_name %}
362
+
<strong>{{display_name}}</strong>
363
+
{% else %}
364
+
<strong>friend</strong>
365
+
{% endif %}.
366
+
What's your status today??
367
+
</div>
368
+
<div>
369
+
<button type="submit">Log out</button>
370
+
</div>
371
+
</form>
372
+
{% else %}
373
+
<div class="session-form">
374
+
<div><a href="/login">Log in</a> to set your status!</div>
375
+
<div>
376
+
<a href="/login" class="button">Log in</a>
377
+
</div>
378
+
</div>
379
+
{% endif %}
380
+
</div>
381
+
```
382
+
383
+
## Step 4. Reading & writing records
384
+
385
+
You can think of the user repositories as collections of JSON records:
386
+
387
+

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

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

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

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

959
+
960
+
## Step 8. Optimistic updates
961
+
962
+
As a final optimization, let's introduce "optimistic updates."
963
+
964
+
Remember the information flow loop with the repo write and the event log?
965
+
966
+

967
+
968
+
Since we're updating our users' repos locally, we can short-circuit that flow to our own database:
969
+
970
+

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

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