// SPDX-FileCopyrightText: 2025 Ɓukasz Niemier <#@hauleth.dev> // // SPDX-License-Identifier: EUPL-1.2 use std::fmt; use std::str::FromStr; use super::helpers; use serde::Deserialize; use simple_eyre::eyre::Result; use ssh_key::PublicKey; #[derive(Debug)] pub struct Did { method: String, id: String, } impl FromStr for Did { type Err = (); fn from_str(input: &str) -> Result { if !input.starts_with("did:") { return Err(()); } if input.ends_with(":") { return Err(()); } let chunks: Box<[_]> = input.splitn(3, ":").collect(); if chunks.len() != 3 { return Err(()); } Ok(Did { method: chunks[1].into(), id: chunks[2].into(), }) } } impl fmt::Display for Did { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "did:{}:{}", self.method, self.id) } } #[derive(Debug)] pub struct Handle(String); impl fmt::Display for Handle { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "{}", self.0) } } impl FromStr for Handle { type Err = (); fn from_str(input: &str) -> Result { if input.len() > 253 { return Err(()); } let input = { let mut input = input.to_ascii_lowercase(); if input.starts_with("@") { input.remove(0); } input }; let segments: Box<[_]> = input.split('.').collect(); if segments.len() < 2 { return Err(()); } if !segments.iter().all(|&s| legal_segment(s)) { return Err(()); } if segments.last().unwrap().as_bytes()[0].is_ascii_digit() { return Err(()); } Ok(Handle(input)) } } pub struct InvalidHandle(String); impl fmt::Display for InvalidHandle { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { write!(f, "Invalid handle: {}", self.0) } } #[derive(Debug)] pub enum Identifier { Did(Did), Handle(Handle), } impl FromStr for Identifier { type Err = InvalidHandle; fn from_str(input: &str) -> Result { input .parse() .map(Identifier::Did) .or_else(|_| input.parse().map(Identifier::Handle)) .map_err(|_| InvalidHandle(input.into())) } } impl fmt::Display for Identifier { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match *self { Identifier::Did(ref did) => write!(f, "{}", did), Identifier::Handle(ref handle) => write!(f, "{}", handle), } } } #[derive(Debug, Deserialize)] pub struct ATProto { #[serde(default = "default_atproto")] pub host: String, #[serde(deserialize_with = "helpers::from_str")] pub handle: Identifier, } impl FromStr for ATProto { type Err = InvalidHandle; fn from_str(input: &str) -> Result { Ok(ATProto { host: default_atproto(), handle: input.parse().map_err(|_| InvalidHandle(input.into()))?, }) } } fn legal_segment(segment: &str) -> bool { let bytes = segment.as_bytes(); !segment.is_empty() && bytes.iter().all(|&b| allowed_byte(b)) && *bytes.first().unwrap() != b'-' && *bytes.last().unwrap() != b'-' } fn allowed_byte(c: u8) -> bool { c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'-' } fn default_atproto() -> String { "https://bsky.social/".into() } mod resp { use serde::Deserialize; use ssh_key::PublicKey; #[derive(Debug, Deserialize)] pub struct Resp { pub records: Box<[Record]>, } #[derive(Debug, Deserialize)] pub struct Record { value: Value, } #[derive(Debug, Deserialize)] pub struct Value { key: String, } impl TryFrom<&Record> for PublicKey { type Error = ssh_key::Error; fn try_from(val: &Record) -> ssh_key::Result { PublicKey::from_openssh(&val.value.key) } } } impl super::Fetch for ATProto { fn fetch(&self) -> Result> { let url = format!( "{host}/xrpc/com.atproto.repo.listRecords?repo={handle}&collection=sh.tangled.publicKey", host = self.host.trim_end_matches('/'), handle = self.handle ); use ureq::tls::{RootCerts, TlsConfig, TlsProvider}; let config = ureq::Agent::config_builder() .tls_config( TlsConfig::builder() .root_certs(RootCerts::PlatformVerifier) .provider(TlsProvider::NativeTls) .build(), ) .build(); let agent = config.new_agent(); let data = agent .get(&url.to_string()) .call()? .body_mut() .read_to_string()?; let decoded: resp::Resp = serde_json::from_str(&data)?; decoded .records .iter() .map(|val| val.try_into().map_err(Into::into)) .collect() } }