profile.rs (4459B)
1 use crate::domain::events::ToRadrootsProfileEventIndex; 2 use crate::relay::event::RelayIndexerEvent; 3 use crate::utils::nostr::normalize_nip05; 4 use std::collections::BTreeMap; 5 6 #[derive(Clone)] 7 struct Nip05Info { 8 full: String, 9 local: String, 10 index_key: String, 11 } 12 13 #[derive(Default, Clone)] 14 pub struct ProfileResolver { 15 author_to_nip05: BTreeMap<String, Nip05Info>, 16 } 17 18 impl ProfileResolver { 19 pub fn from_metadata(raw_metadata: &[RelayIndexerEvent]) -> Self { 20 let mut latest: BTreeMap<String, (u32, String, Nip05Info)> = BTreeMap::new(); 21 22 for raw in raw_metadata { 23 if let Ok(evt) = raw.to_radroots_profile_event() { 24 if let Some(n) = &evt.metadata.profile.nip05 { 25 let (full, local, index_key) = normalize_nip05(n); 26 if index_key.is_empty() { 27 continue; 28 } 29 30 let author = evt.event.author.to_lowercase(); 31 let ts: u32 = evt.metadata.published_at; 32 let event_id = evt.event.id.clone(); 33 let should_replace = match latest.get(&author) { 34 None => true, 35 Some((old_ts, old_id, _)) => { 36 ts > *old_ts || (ts == *old_ts && event_id < *old_id) 37 } 38 }; 39 if should_replace { 40 latest.insert( 41 author, 42 ( 43 ts, 44 event_id, 45 Nip05Info { 46 full, 47 local, 48 index_key, 49 }, 50 ), 51 ); 52 } 53 } 54 } 55 } 56 57 let author_to_nip05 = latest 58 .into_iter() 59 .map(|(a, (_ts, _id, n))| (a, n)) 60 .collect(); 61 62 Self { author_to_nip05 } 63 } 64 65 #[inline] 66 pub fn nip05_for_author(&self, author_hex: &str) -> Option<&str> { 67 self.author_to_nip05 68 .get(author_hex) 69 .map(|info| info.index_key.as_str()) 70 } 71 72 #[inline] 73 pub fn nip05_full_for_author(&self, author_hex: &str) -> Option<&str> { 74 self.author_to_nip05 75 .get(author_hex) 76 .map(|info| info.full.as_str()) 77 } 78 79 #[inline] 80 pub fn nip05_local_for_author(&self, author_hex: &str) -> Option<&str> { 81 self.author_to_nip05 82 .get(author_hex) 83 .map(|info| info.local.as_str()) 84 } 85 } 86 87 #[cfg(test)] 88 mod tests { 89 use super::ProfileResolver; 90 use crate::domain::indexer::kind::IndexerEventKind; 91 use crate::relay::event::RelayIndexerEvent; 92 93 fn make_profile_event(id: &str, author: &str, created_at: u32, nip05: &str) -> RelayIndexerEvent { 94 let content = format!(r#"{{"name":"user","nip05":"{}"}}"#, nip05); 95 RelayIndexerEvent { 96 id: id.to_string(), 97 author: author.to_string(), 98 created_at, 99 pubkey: author.to_string(), 100 kind: IndexerEventKind::Profile, 101 tags: Vec::new(), 102 content, 103 hash: id.to_string(), 104 sig: "sig".to_string(), 105 } 106 } 107 108 #[test] 109 fn resolver_tiebreaks_by_event_id() { 110 let author = "a".repeat(64); 111 let high = make_profile_event("f".repeat(64).as_str(), &author, 10, "high@radroots.market"); 112 let low = make_profile_event("0".repeat(64).as_str(), &author, 10, "low@radroots.market"); 113 114 let resolver = ProfileResolver::from_metadata(&[high, low]); 115 let full = resolver 116 .nip05_full_for_author(&author) 117 .expect("full nip05"); 118 assert_eq!(full, "low@radroots.market"); 119 } 120 121 #[test] 122 fn resolver_returns_local_and_index_key() { 123 let author = "b".repeat(64); 124 let event = make_profile_event("1".repeat(64).as_str(), &author, 10, "user@radroots.market"); 125 let resolver = ProfileResolver::from_metadata(&[event]); 126 assert_eq!( 127 resolver.nip05_full_for_author(&author), 128 Some("user@radroots.market") 129 ); 130 assert_eq!(resolver.nip05_local_for_author(&author), Some("user")); 131 assert_eq!(resolver.nip05_for_author(&author), Some("user")); 132 } 133 }