Scalable and distributed custom feed generator, ott - on that topic
1use std::collections::BTreeMap;
2
3use axum::{routing::get, Json, Router};
4use jacquard::types::did_doc::{DidDocument, Service, VerificationMethod};
5use jacquard_api::app_bsky::feed::{
6 get_feed_skeleton::{GetFeedSkeletonOutput, GetFeedSkeletonRequest},
7 SkeletonFeedPost,
8};
9use jacquard_axum::did_web::did_web_router;
10use jacquard_axum::ExtractXrpc;
11use jacquard_axum::{
12 service_auth::{ExtractServiceAuth, ServiceAuthConfig},
13 IntoRouter,
14};
15use jacquard_common::types::string::Did;
16use jacquard_identity::resolver::ResolverOptions;
17use jacquard_identity::JacquardResolver;
18use ott_xrpc::{bsky::BskyClient, key::generate_key};
19
20use serde_json::Value;
21use tracing::info;
22use tracing_subscriber::EnvFilter;
23
24use tower_http::normalize_path::NormalizePathLayer;
25
26async fn handle_wellknown_atproto_did() -> Json<serde_json::Value> {
27 Json::from(Value::from("did:web:ott.aleeve.dev"))
28}
29
30async fn handler(
31 ExtractServiceAuth(auth): ExtractServiceAuth,
32 ExtractXrpc(args): ExtractXrpc<GetFeedSkeletonRequest>,
33) -> Result<Json<GetFeedSkeletonOutput<'static>>, String> {
34 let posts: Vec<SkeletonFeedPost<'static>> = vec![SkeletonFeedPost {
35 post: "at://did:plc:klugggc44dmpomjkuzyahzjd/app.bsky.feed.post/3m2y6a5h6os27"
36 .parse()
37 .map_err(|_| "Failed to parse uri".to_string())?,
38 feed_context: None,
39 extra_data: BTreeMap::default(),
40 reason: None,
41 }];
42
43 let output = GetFeedSkeletonOutput::<'static> {
44 feed: posts,
45 cursor: None,
46 req_id: None,
47 extra_data: BTreeMap::default(),
48 };
49 Ok(Json(output.clone()))
50}
51
52#[tokio::main]
53async fn main() {
54 tracing_subscriber::fmt()
55 .with_ansi(true) // Colors enabled (default)
56 .with_max_level(tracing::Level::INFO)
57 .init();
58
59 info!("Setup");
60 let did_str = "did:web:ott.aleeve.dev";
61 let did = Did::new_static(did_str);
62
63 let verification_method = VerificationMethod {
64 id: format!("{}#atproto", did_str).into(),
65 r#type: "Multikey".into(),
66 controller: Some("did:web:ott.aleeve.dev".into()),
67 public_key_multibase: Some(generate_key().into()),
68 extra_data: BTreeMap::default(),
69 };
70
71 let service = Service {
72 id: "#bsky_fg".into(),
73 service_endpoint: Some("https://ott.aleeve.dev".into()),
74 r#type: "BskyFeedGenerator".into(),
75 extra_data: BTreeMap::default(),
76 };
77
78 let did_doc: DidDocument = DidDocument {
79 id: did.clone().unwrap(),
80 also_known_as: Some(vec!["at://ott.aleeve.dev".into()]),
81 verification_method: Some(vec![verification_method]),
82 service: Some(vec![service]),
83 extra_data: BTreeMap::default(),
84 };
85
86 let resolver = JacquardResolver::new(reqwest::Client::new(), ResolverOptions::default());
87 let config: ServiceAuthConfig<JacquardResolver> =
88 ServiceAuthConfig::new(did.clone().unwrap(), resolver);
89
90 let app = Router::new()
91 .merge(GetFeedSkeletonRequest::into_router(handler))
92 .with_state(config)
93 .merge(did_web_router(did_doc))
94 .route(
95 "/.well-known/atproto-did",
96 get(handle_wellknown_atproto_did),
97 )
98 .layer(NormalizePathLayer::trim_trailing_slash());
99
100 let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
101 info!("Starting service");
102 axum::serve(listener, app).await.unwrap();
103}