An atproto server implementing resolveDid without auth (Kind of. Badly).
resolvedid.rs
159 lines 5.2 kB view raw
1use std::{error::Error, ops::Deref}; 2 3use axum::{Json, Router, extract::FromRequestParts, http::StatusCode, response::IntoResponse}; 4 5use jacquard::{ 6 IntoStatic, 7 identity::resolver::{self, IdentityError, IdentityResolver}, 8 types::value::Data, 9}; 10use jacquard_api::com_atproto::identity::resolve_did::{ 11 ResolveDid, ResolveDidError, ResolveDidOutput, 12}; 13use jacquard_common::types::xrpc::{XrpcMethod, XrpcRequest}; 14use miette::{IntoDiagnostic, Result}; 15use serde::{Deserialize, Serialize}; 16use tracing_subscriber::EnvFilter; 17 18trait IntoRouter { 19 fn into_router<T, S, U>(handler: U) -> Router<S> 20 where 21 T: 'static, 22 S: Clone + Send + Sync + 'static, 23 U: axum::handler::Handler<T, S>; 24} 25 26impl<X: XrpcRequest> IntoRouter for X { 27 /// Creates an axum router that will invoke `handler` in response to xrpc 28 /// request `X`. 29 fn into_router<T, S, U>(handler: U) -> Router<S> 30 where 31 T: 'static, 32 S: Clone + Send + Sync + 'static, 33 U: axum::handler::Handler<T, S>, 34 { 35 Router::new().route( 36 // this should really be a compile time operation, but I cannot 37 // figure out how to make that happen 38 format!("/xrpc/{}", X::NSID).as_str(), 39 (match X::METHOD { 40 XrpcMethod::Query => axum::routing::get, 41 XrpcMethod::Procedure(_) => axum::routing::put, 42 })(handler), 43 ) 44 } 45} 46 47struct XrpcArgs<T>(T); 48impl<T> Deref for XrpcArgs<T> { 49 type Target = T; 50 51 fn deref(&self) -> &Self::Target { 52 &self.0 53 } 54} 55 56#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, thiserror::Error)] 57#[serde(tag = "error", content = "message")] 58pub enum AtProtoGenericError { 59 #[error("Invalid Request")] 60 InvalidRequest(Option<String>), 61 #[error("Expired Token")] 62 ExpiredToken(Option<String>), 63 #[error("Invalid Token")] 64 InvalidToken(Option<String>), 65} 66 67#[derive(thiserror::Error, Debug)] 68#[error(transparent)] 69struct XrpcError<T: std::error::Error>(pub T); 70 71impl<T: Serialize + Error> IntoResponse for XrpcError<T> { 72 fn into_response(self) -> axum::response::Response { 73 (StatusCode::BAD_REQUEST, Json(self.0)).into_response() 74 } 75} 76 77impl From<AtProtoGenericError> for XrpcError<AtProtoGenericError> { 78 fn from(value: AtProtoGenericError) -> Self { 79 Self(value) 80 } 81} 82impl<'a> From<ResolveDidError<'a>> for XrpcError<ResolveDidError<'static>> { 83 fn from(value: ResolveDidError) -> Self { 84 Self(value.into_static()) 85 } 86} 87 88impl From<IdentityError> for XrpcError<ResolveDidError<'static>> { 89 fn from(value: IdentityError) -> Self { 90 Self(ResolveDidError::DidNotFound(Some(format!("{}", value)))) 91 } 92} 93 94struct XrpcResponse<T>(T); 95 96impl<T: Serialize> IntoResponse for XrpcResponse<T> { 97 fn into_response(self) -> axum::response::Response { 98 Json(self.0).into_response() 99 } 100} 101 102impl<'a> From<ResolveDidOutput<'a>> for XrpcResponse<ResolveDidOutput<'static>> { 103 fn from(value: ResolveDidOutput<'a>) -> Self { 104 Self(value.into_static()) 105 } 106} 107 108impl From<serde_html_form::de::Error> for XrpcError<AtProtoGenericError> { 109 fn from(_: serde_html_form::de::Error) -> Self { 110 AtProtoGenericError::InvalidRequest(None).into() 111 } 112} 113 114impl<S: Send + Sync> FromRequestParts<S> for XrpcArgs<ResolveDid<'static>> { 115 type Rejection = XrpcError<AtProtoGenericError>; 116 117 async fn from_request_parts( 118 parts: &mut axum::http::request::Parts, 119 _state: &S, 120 ) -> std::result::Result<Self, Self::Rejection> { 121 let q = parts.uri.query().unwrap_or_default(); 122 let de = serde_html_form::Deserializer::new(url::form_urlencoded::parse(q.as_bytes())); 123 let value = ResolveDid::deserialize(de)?; 124 Ok(Self(value.into_static())) 125 } 126} 127 128#[tokio::main] 129async fn main() -> Result<()> { 130 tracing_subscriber::fmt() 131 .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc_3339()) 132 .with_env_filter(EnvFilter::from_env("QDPDS_LOG")) 133 .init(); 134 let app = 135 Router::new() 136 .route("/", axum::routing::get(|| async { "hello world!" })) 137 .merge(ResolveDid::into_router( 138 async move |XrpcArgs(args): XrpcArgs<ResolveDid<'static>>| -> Result< 139 XrpcResponse<ResolveDidOutput>, 140 XrpcError<ResolveDidError>, 141 > { 142 let res = resolver::slingshot_resolver_default(); 143 let doc = res.resolve_did_doc(&args.did).await?; 144 let valid_doc = doc.parse()?; 145 let doc_value = serde_json::to_value(valid_doc).unwrap(); 146 Ok(ResolveDidOutput { 147 did_doc: Data::from_json(&doc_value).unwrap().into_static(), 148 extra_data: Default::default(), 149 } 150 .into()) 151 }, 152 )) 153 .layer(tower_http::trace::TraceLayer::new_for_http()); 154 let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") 155 .await 156 .into_diagnostic()?; 157 axum::serve(listener, app).await.unwrap(); 158 Ok(()) 159}