use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Nip11Info { pub name: Option, pub description: Option, pub supported_nips: Option>, pub supported_grasps: Option>, pub software: Option, pub version: Option, } #[derive(Debug, Clone)] pub struct GraspServer { pub domain: String, pub relay_url: String, pub clone_url_prefix: String, pub nip11: Option, pub healthy: bool, } impl GraspServer { pub fn from_domain(domain: &str) -> Self { let clean = domain .trim_start_matches("https://") .trim_start_matches("http://") .trim_end_matches('/') .to_string(); Self { relay_url: format!("wss://{}", clean), clone_url_prefix: format!("https://{}", clean), nip11: None, healthy: false, domain: clean, } } pub fn clone_url(&self, npub_hex: &str, identifier: &str) -> String { format!("{}/{}/{}.git", self.clone_url_prefix, npub_hex, identifier) } pub fn is_grasp_server(&self) -> bool { self.nip11 .as_ref() .map(|info| info.supported_grasps.is_some()) .unwrap_or(false) } } pub async fn verify_grasp_server(domain: &str) -> Result { let mut server = GraspServer::from_domain(domain); let nip11_url = format!("https://{}", server.domain); let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(10)) .build()?; let resp = client .get(&nip11_url) .header("Accept", "application/nostr+json") .send() .await; match resp { Ok(resp) if resp.status().is_success() => { match resp.json::().await { Ok(info) => { let is_grasp = info.supported_grasps.is_some(); if is_grasp { tracing::info!( domain = %server.domain, grasps = ?info.supported_grasps, version = ?info.version, "verified GRASP server" ); } else { tracing::warn!( domain = %server.domain, "server responded to NIP-11 but has no supported_grasps" ); } server.healthy = is_grasp; server.nip11 = Some(info); } Err(e) => { tracing::warn!(domain = %server.domain, error = %e, "failed to parse NIP-11 response"); } } } Ok(resp) => { tracing::warn!( domain = %server.domain, status = %resp.status(), "NIP-11 check returned non-success" ); } Err(e) => { tracing::warn!(domain = %server.domain, error = %e, "NIP-11 check failed"); } } Ok(server) } pub async fn verify_all_servers(domains: &[String]) -> HashMap { let mut servers = HashMap::new(); let mut set = tokio::task::JoinSet::new(); for d in domains { let domain = d.clone(); set.spawn(async move { let result = verify_grasp_server(&domain).await; (domain, result) }); } while let Some(res) = set.join_next().await { match res { Ok((domain, result)) => match result { Ok(server) => { servers.insert(domain, server); } Err(e) => { tracing::error!(domain = %domain, error = %e, "failed to verify server"); } }, Err(e) => { tracing::error!(error = %e, "task panicked during server verification"); } } } servers }