Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
1use clap::Parser; 2use poem::{ 3 EndpointExt, Response, Route, Server, get, handler, 4 http::StatusCode, 5 listener::TcpListener, 6 middleware::{AddData, Tracing}, 7 web::{Data, Json, Query, TypedHeader, headers::Host}, 8}; 9use serde::{Deserialize, Serialize}; 10 11#[handler] 12fn hello() -> String { 13 "ɹoʇɔǝʅⅎǝɹ".to_string() 14} 15 16#[derive(Debug, Serialize)] 17struct DidDoc { 18 id: String, 19 service: [DidService; 1], 20} 21 22#[derive(Debug, Clone, Serialize)] 23#[serde(rename_all = "camelCase")] 24struct DidService { 25 id: String, 26 r#type: String, 27 service_endpoint: String, 28} 29 30#[handler] 31fn did_doc(TypedHeader(host): TypedHeader<Host>, service: Data<&DidService>) -> Json<DidDoc> { 32 Json(DidDoc { 33 id: format!("did:web:{}", host.hostname()), 34 service: [service.clone()], 35 }) 36} 37 38#[derive(Deserialize)] 39struct AskQuery { 40 domain: String, 41} 42#[handler] 43fn ask_caddy( 44 Data(parent): Data<&Option<String>>, 45 Query(AskQuery { domain }): Query<AskQuery>, 46) -> Response { 47 if let Some(parent) = parent { 48 if let Some(prefix) = domain.strip_suffix(&format!(".{parent}")) { 49 if !prefix.contains('.') { 50 // no sub-sub-domains allowed 51 return Response::builder().body("ok"); 52 } 53 } 54 }; 55 Response::builder() 56 .status(StatusCode::FORBIDDEN) 57 .body("nope") 58} 59 60/// Slingshot record edge cache 61#[derive(Parser, Debug, Clone)] 62#[command(version, about, long_about = None)] 63struct Args { 64 /// The DID document service ID to serve 65 /// 66 /// must start with a '#', like `#bsky_appview' 67 #[arg(long)] 68 id: String, 69 /// Service type 70 /// 71 /// Not sure exactly what its requirements are. 'BlueskyAppview' for example 72 #[arg(long)] 73 r#type: String, 74 /// The HTTPS endpoint for the service 75 #[arg(long)] 76 service_endpoint: String, 77 /// The parent domain; requests should come from subdomains of this 78 #[arg(long)] 79 domain: Option<String>, 80} 81 82impl From<Args> for DidService { 83 fn from(a: Args) -> Self { 84 Self { 85 id: a.id, 86 r#type: a.r#type, 87 service_endpoint: a.service_endpoint, 88 } 89 } 90} 91 92#[tokio::main(flavor = "current_thread")] 93async fn main() { 94 tracing_subscriber::fmt::init(); 95 log::info!("ɹoʇɔǝʅⅎǝɹ"); 96 97 let args = Args::parse(); 98 let domain = args.domain.clone(); 99 let service: DidService = args.into(); 100 101 Server::new(TcpListener::bind("0.0.0.0:3001")) 102 .run( 103 Route::new() 104 .at("/", get(hello)) 105 .at("/.well-known/did.json", get(did_doc)) 106 .at("/ask", get(ask_caddy)) 107 .with(AddData::new(service)) 108 .with(AddData::new(domain)) 109 .with(Tracing), 110 ) 111 .await 112 .unwrap() 113}