diff options
Diffstat (limited to 'src/health.rs')
| -rw-r--r-- | src/health.rs | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/src/health.rs b/src/health.rs new file mode 100644 index 0000000..2853239 --- /dev/null +++ b/src/health.rs | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | use anyhow::{Context, Result}; | ||
| 2 | use serde::{Deserialize, Serialize}; | ||
| 3 | use std::collections::HashMap; | ||
| 4 | |||
| 5 | #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| 6 | pub struct Nip11Info { | ||
| 7 | pub name: Option<String>, | ||
| 8 | pub description: Option<String>, | ||
| 9 | pub supported_nips: Option<Vec<u64>>, | ||
| 10 | pub supported_grasps: Option<Vec<String>>, | ||
| 11 | pub software: Option<String>, | ||
| 12 | pub version: Option<String>, | ||
| 13 | } | ||
| 14 | |||
| 15 | #[derive(Debug, Clone)] | ||
| 16 | pub struct GraspServer { | ||
| 17 | pub domain: String, | ||
| 18 | pub relay_url: String, | ||
| 19 | pub clone_url_prefix: String, | ||
| 20 | pub nip11: Option<Nip11Info>, | ||
| 21 | pub healthy: bool, | ||
| 22 | } | ||
| 23 | |||
| 24 | impl GraspServer { | ||
| 25 | pub fn from_domain(domain: &str) -> Self { | ||
| 26 | let clean = domain | ||
| 27 | .trim_start_matches("https://") | ||
| 28 | .trim_start_matches("http://") | ||
| 29 | .trim_end_matches('/') | ||
| 30 | .to_string(); | ||
| 31 | Self { | ||
| 32 | relay_url: format!("wss://{}", clean), | ||
| 33 | clone_url_prefix: format!("https://{}", clean), | ||
| 34 | nip11: None, | ||
| 35 | healthy: false, | ||
| 36 | domain: clean, | ||
| 37 | } | ||
| 38 | } | ||
| 39 | |||
| 40 | pub fn clone_url(&self, npub_hex: &str, identifier: &str) -> String { | ||
| 41 | format!("{}/{}/{}.git", self.clone_url_prefix, npub_hex, identifier) | ||
| 42 | } | ||
| 43 | |||
| 44 | pub fn is_grasp_server(&self) -> bool { | ||
| 45 | self.nip11 | ||
| 46 | .as_ref() | ||
| 47 | .map(|info| info.supported_grasps.is_some()) | ||
| 48 | .unwrap_or(false) | ||
| 49 | } | ||
| 50 | } | ||
| 51 | |||
| 52 | pub async fn verify_grasp_server(domain: &str) -> Result<GraspServer> { | ||
| 53 | let mut server = GraspServer::from_domain(domain); | ||
| 54 | let nip11_url = format!("https://{}", server.domain); | ||
| 55 | |||
| 56 | let client = reqwest::Client::builder() | ||
| 57 | .timeout(std::time::Duration::from_secs(10)) | ||
| 58 | .build()?; | ||
| 59 | |||
| 60 | let resp = client | ||
| 61 | .get(&nip11_url) | ||
| 62 | .header("Accept", "application/nostr+json") | ||
| 63 | .send() | ||
| 64 | .await; | ||
| 65 | |||
| 66 | match resp { | ||
| 67 | Ok(resp) if resp.status().is_success() => { | ||
| 68 | match resp.json::<Nip11Info>().await { | ||
| 69 | Ok(info) => { | ||
| 70 | let is_grasp = info.supported_grasps.is_some(); | ||
| 71 | if is_grasp { | ||
| 72 | tracing::info!( | ||
| 73 | domain = %server.domain, | ||
| 74 | grasps = ?info.supported_grasps, | ||
| 75 | version = ?info.version, | ||
| 76 | "verified GRASP server" | ||
| 77 | ); | ||
| 78 | } else { | ||
| 79 | tracing::warn!( | ||
| 80 | domain = %server.domain, | ||
| 81 | "server responded to NIP-11 but has no supported_grasps" | ||
| 82 | ); | ||
| 83 | } | ||
| 84 | server.healthy = is_grasp; | ||
| 85 | server.nip11 = Some(info); | ||
| 86 | } | ||
| 87 | Err(e) => { | ||
| 88 | tracing::warn!(domain = %server.domain, error = %e, "failed to parse NIP-11 response"); | ||
| 89 | } | ||
| 90 | } | ||
| 91 | } | ||
| 92 | Ok(resp) => { | ||
| 93 | tracing::warn!( | ||
| 94 | domain = %server.domain, | ||
| 95 | status = %resp.status(), | ||
| 96 | "NIP-11 check returned non-success" | ||
| 97 | ); | ||
| 98 | } | ||
| 99 | Err(e) => { | ||
| 100 | tracing::warn!(domain = %server.domain, error = %e, "NIP-11 check failed"); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | Ok(server) | ||
| 105 | } | ||
| 106 | |||
| 107 | pub async fn verify_all_servers(domains: &[String]) -> HashMap<String, GraspServer> { | ||
| 108 | let mut servers = HashMap::new(); | ||
| 109 | let mut set = tokio::task::JoinSet::new(); | ||
| 110 | |||
| 111 | for d in domains { | ||
| 112 | let domain = d.clone(); | ||
| 113 | set.spawn(async move { | ||
| 114 | let result = verify_grasp_server(&domain).await; | ||
| 115 | (domain, result) | ||
| 116 | }); | ||
| 117 | } | ||
| 118 | |||
| 119 | while let Some(res) = set.join_next().await { | ||
| 120 | match res { | ||
| 121 | Ok((domain, result)) => match result { | ||
| 122 | Ok(server) => { | ||
| 123 | servers.insert(domain, server); | ||
| 124 | } | ||
| 125 | Err(e) => { | ||
| 126 | tracing::error!(domain = %domain, error = %e, "failed to verify server"); | ||
| 127 | } | ||
| 128 | }, | ||
| 129 | Err(e) => { | ||
| 130 | tracing::error!(error = %e, "task panicked during server verification"); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | servers | ||
| 136 | } | ||