very fast at protocol indexer with flexible filtering, xrpc queries, cursor-backed event stream, and more, built on fjall
rust fjall at-protocol atproto indexer
59
fork

Configure Feed

Select the types of activity you want to include in your feed.

[api] implement com.atproto.repo.describeRepo

dawn 7fe0698e 6ffaf685

+105 -16
+1
README.md
··· 307 307 the following are implemented currently: 308 308 - `com.atproto.repo.getRecord` 309 309 - `com.atproto.repo.listRecords` 310 + - `com.atproto.repo.describeRepo` (also see `systems.gaze.hydrant.describeRepo`) 310 311 - `com.atproto.sync.getRepo` (`since` parameter not implemented!) 311 312 - `com.atproto.sync.getHostStatus` 312 313 - `com.atproto.sync.listHosts`
+49
src/api/xrpc/com_atproto_describe_repo.rs
··· 1 + use futures::TryFutureExt; 2 + use jacquard_api::com_atproto::repo::describe_repo::{DescribeRepoOutput, DescribeRepoRequest}; 3 + 4 + use crate::util::invalid_handle; 5 + 6 + use super::*; 7 + 8 + pub async fn handle( 9 + State(hydrant): State<Hydrant>, 10 + ExtractXrpc(req): ExtractXrpc<DescribeRepoRequest>, 11 + ) -> XrpcResult<Json<DescribeRepoOutput<'static>>> { 12 + let nsid = "com.atproto.repo.describeRepo"; 13 + let resolver = &hydrant.state.resolver; 14 + 15 + let did = resolver 16 + .resolve_did(&req.repo) 17 + .await 18 + .map_err(|e| bad_request(nsid, format!("could not resolve identifier: {e}")))?; 19 + 20 + let repo = hydrant.repos.get(&did); 21 + let ((did_doc, handle, handle_is_correct), collections) = tokio::try_join!( 22 + async { 23 + let (doc, handle) = resolver 24 + .resolve_raw_doc(&did) 25 + .map_err(|e| upstream_error(nsid, format!("could not resolve DID document: {e}"))) 26 + .await?; 27 + let (handle, handle_is_correct) = if let Some(h) = handle { 28 + let is_correct = resolver 29 + .verify_handle(&did, &h) 30 + .map_err(|e| bad_request(nsid, format!("could not verify handle {h}: {e}"))) 31 + .await?; 32 + (h, is_correct) 33 + } else { 34 + (invalid_handle(), false) 35 + }; 36 + Ok((doc, handle, handle_is_correct)) 37 + }, 38 + repo.collections().map_err(|e| internal_error(nsid, e)), 39 + )?; 40 + 41 + Ok(Json(DescribeRepoOutput { 42 + did, 43 + handle, 44 + handle_is_correct, 45 + did_doc, 46 + collections: collections.into_iter().map(|(k, _)| k).collect(), 47 + extra_data: Default::default(), 48 + })) 49 + }
+6
src/api/xrpc/mod.rs
··· 6 6 use axum::routing::get; 7 7 use axum::{Json, Router, extract::State, http::StatusCode}; 8 8 use jacquard_api::com_atproto::repo::{ 9 + describe_repo::DescribeRepoRequest as AtprotoDescribeRepoRequest, 9 10 get_record::{GetRecordError, GetRecordOutput, GetRecordRequest}, 10 11 list_records::{ListRecordsOutput, ListRecordsRequest, Record as RepoRecord}, 11 12 }; ··· 27 28 use smol_str::ToSmolStr; 28 29 use std::fmt::Display; 29 30 31 + mod com_atproto_describe_repo; 30 32 mod count_records; 31 33 mod describe_repo; 32 34 mod get_host_status; ··· 44 46 .route(ListRecordsRequest::PATH, get(list_records::handle)) 45 47 .route(CountRecords::PATH, get(count_records::handle)) 46 48 .route(DescribeRepo::PATH, get(describe_repo::handle)) 49 + .route( 50 + AtprotoDescribeRepoRequest::PATH, 51 + get(com_atproto_describe_repo::handle), 52 + ) 47 53 .route(GetHostStatusRequest::PATH, get(get_host_status::handle)) 48 54 .route(ListHostsRequest::PATH, get(list_hosts::handle)) 49 55 .route(GetLatestCommitRequest::PATH, get(get_latest_commit::handle))
+5 -15
src/control/repos.rs
··· 20 20 use crate::db::{self, Db, keys, ser_repo_state}; 21 21 use crate::state::AppState; 22 22 use crate::types::{GaugeState, RepoState, RepoStatus}; 23 + use crate::util::invalid_handle; 23 24 24 25 /// information about a tracked or known repository. returned by [`ReposControl`] methods. 25 26 #[derive(Debug, Clone, serde::Serialize)] ··· 513 514 514 515 /// returns a bi-directionally validated mini doc. 515 516 pub async fn mini_doc(&self) -> Result<MiniDoc<'static>, MiniDocError> { 516 - fn invalid_handle() -> Handle<'static> { 517 - unsafe { Handle::unchecked("handle.invalid") } 518 - } 519 - 520 517 let Some(info) = self.info().await.map_err(MiniDocError::Other)? else { 521 518 return Err(MiniDocError::RepoNotFound); 522 519 }; ··· 533 530 .ok_or_else(|| MiniDocError::CouldNotResolveIdentity)? 534 531 .into_static(); 535 532 536 - let handle = if let Some(handle_unverified) = info.handle { 537 - let id = AtIdentifier::Handle(handle_unverified); 538 - let handle_did = self 533 + let handle = if let Some(h) = info.handle { 534 + let is_valid = self 539 535 .state 540 536 .resolver 541 - .resolve_did(&id) 537 + .verify_handle(&self.did, &h) 542 538 .await 543 539 .into_diagnostic() 544 540 .map_err(MiniDocError::Other)?; 545 - 546 - (handle_did == self.did) 547 - .then(|| match id { 548 - AtIdentifier::Handle(h) => h, 549 - _ => unreachable!("can only be handle"), 550 - }) 551 - .unwrap_or_else(invalid_handle) 541 + is_valid.then_some(h).unwrap_or_else(invalid_handle) 552 542 } else { 553 543 invalid_handle() 554 544 };
+39
src/resolver.rs
··· 4 4 use std::sync::atomic::{AtomicUsize, Ordering}; 5 5 use std::time::Duration; 6 6 7 + use jacquard_common::Data; 7 8 use jacquard_common::IntoStatic; 8 9 use jacquard_common::types::crypto::PublicKey; 9 10 use jacquard_common::types::ident::AtIdentifier; ··· 203 204 .key 204 205 .ok_or_else(|| NoSigningKeyError(did)) 205 206 .into_diagnostic()?) 207 + } 208 + 209 + /// resolves the full DID document as raw [`Data`] without caching, and 210 + /// extracts the first `alsoKnownAs` handle if present. 211 + pub async fn resolve_raw_doc( 212 + &self, 213 + did: &Did<'_>, 214 + ) -> Result<(Data<'static>, Option<Handle<'static>>), ResolverError> { 215 + let doc_resp = self 216 + .req(did.starts_with("did:plc:"), |j| j.resolve_did_doc(did)) 217 + .await?; 218 + 219 + let handle = { 220 + let parsed = doc_resp.parse(); 221 + parsed.ok().and_then(|doc| { 222 + let mut handles = doc.handles(); 223 + handles 224 + .is_empty() 225 + .not() 226 + .then(|| handles.remove(0).into_static()) 227 + }) 228 + }; 229 + 230 + let data: Data<'_> = serde_json::from_slice(&doc_resp.buffer) 231 + .map_err(|e| ResolverError::Generic(miette::miette!("failed to parse DID doc: {e}")))?; 232 + 233 + Ok((data.into_static(), handle)) 234 + } 235 + 236 + /// returns `true` if the given handle bi-directionally resolves to `did`. 237 + pub async fn verify_handle( 238 + &self, 239 + did: &Did<'_>, 240 + handle: &Handle<'_>, 241 + ) -> Result<bool, ResolverError> { 242 + let id = AtIdentifier::Handle(handle.clone().into_static()); 243 + let resolved_did = self.resolve_did(&id).await?; 244 + Ok(resolved_did.as_str() == did.as_str()) 206 245 } 207 246 } 208 247
+5 -1
src/util.rs
··· 1 1 use std::time::Duration; 2 2 3 - use jacquard_common::deps::fluent_uri; 3 + use jacquard_common::{deps::fluent_uri, types::string::Handle}; 4 4 use rand::RngExt; 5 5 use reqwest::StatusCode; 6 6 use serde::{Deserialize, Deserializer, Serializer}; ··· 176 176 .expect("that url is validated") 177 177 .to_owned() 178 178 } 179 + 180 + pub(crate) fn invalid_handle() -> Handle<'static> { 181 + unsafe { Handle::unchecked("handle.invalid") } 182 + }