use trust_dns_resolver::Resolver; use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; #[derive(Debug)] pub struct DidDoc { pub did: String, pub pds: String, } fn get_txt_did(handle: &String) -> Result { println!(" Trying dns TXT for _atproto.{}", handle); // create a txt resolver let resolver = match Resolver::new(ResolverConfig::default(), ResolverOpts::default()) { Ok(val) => val, Err(_) => { println!(" Couldn't create a DNS resolver"); return Err(()); } }; // resolve _atproto.handle to a TXT record let txt_res = match resolver.txt_lookup("_atproto.".to_owned() + &handle) { Ok(val) => val, Err(_) => { println!(" Couldn't resolve to a TXT record"); return Err(()); } // collect all entries and convert to strings } .into_iter() .map(|x| x.to_string()) .collect::>(); // filter entries which do not start with `did=` let did_res = txt_res .clone() .extract_if(.., |x| x.starts_with("did=")) .collect::>(); // only 1 did= can exist // https://atproto.com/specs/handle#:~:text=If%20multiple%20valid%20records%20with%20different%20DIDs%20are%20present,%20resolution%20should%20fail. if did_res.len() != 1 { println!(" Found too many DIDs for this handle"); return Err(()); } return Ok(did_res[0][4..].to_string()); } fn get_http_did(handle: &String) -> Result { println!( " Trying https for https://{}/.well-known/atproto-did", handle ); let res = match reqwest::blocking::get("https://".to_owned() + handle + "/.well-known/atproto-did") { Ok(val) => val, Err(_) => { println!(" GET request failed"); return Err(()); } }; // as per spec, non 2xx code means failure if !res.status().is_success() { println!( " Got non 2xx status code: {}", res.status().as_str() ); return Err(()); } let did_unparsed = match res.text() { Ok(val) => val, Err(_) => { println!(" Missing or malformed body response"); return Err(()); } }; let did = did_unparsed.trim(); if !did.starts_with("did:") { println!(" Did not find a DID"); return Err(()); }; return Ok(String::from(did)); } fn parse_doc(did: String, text: String) -> Result { let text_json = match json::parse(&text) { Ok(val) => val, Err(_) => { println!(" Malformed DID document"); return Err(()); } }; for service in text_json["service"].members() { if service["id"] .as_str() .is_some_and(|x| x.ends_with("#atproto_pds")) && service["type"] .as_str() .is_some_and(|x| x == "AtprotoPersonalDataServer") && let Some(pds) = service["serviceEndpoint"].as_str() { return Ok(DidDoc { did: did, pds: pds.to_string(), }); } } println!(" Missing fields from DID document"); return Err(()); } fn get_plc_doc(plc: &str) -> Result { let res = match reqwest::blocking::get("https://plc.directory/did:plc:".to_owned() + plc) { Ok(val) => val, Err(_) => { println!(" GET request failed"); return Err(()); } }; if !res.status().is_success() { println!(" Got non 2xx status code: {}", res.status().as_str()); return Err(()); } return parse_doc( "did:plc:".to_owned() + plc, match res.text() { Ok(val) => val, Err(_) => { println!(" Missing or malformed body response"); return Err(()); } }, ); } fn get_web_doc(web: &str) -> Result { let res = match reqwest::blocking::get("https://".to_owned() + web + "/.well-known/did.json") { Ok(val) => val, Err(_) => { println!(" GET request failed"); return Err(()); } }; if !res.status().is_success() { println!(" Got non 2xx status code: {}", res.status().as_str()); return Err(()); } return parse_doc( "did:web:".to_owned() + web, match res.text() { Ok(val) => val, Err(_) => { println!(" Missing or malformed body response"); return Err(()); } }, ); } pub fn get_did(handle: &String) -> Result { println!(" Getting DID for {}", handle); let did = if let Ok(did) = get_txt_did(&handle) { did } else { if let Ok(did) = get_http_did(&handle) { did } else { println!(" Could not get a DID"); return Err(()); } }; println!(" Getting DID document for {}", did); let did_doc = if did.starts_with("did:plc:") { get_plc_doc(&did[8..]) } else if did.starts_with("did:web:") { get_web_doc(&did[8..]) } else { println!(" Could not get a DID document"); Err(()) }; return did_doc; }