unknowns.rs (10418B)
1 //! Unknown ID collection with relay provenance for fetching missing data. 2 //! 3 //! Adapted from notedeck's unknowns pattern for notecrumbs' one-shot HTTP context. 4 //! Collects unknown note IDs and profile pubkeys from: 5 //! - Quote references (q tags, inline nevent/note/naddr) 6 //! - Mentioned profiles (npub/nprofile in content) 7 //! - Reply chain (e tags with reply/root markers) 8 //! - Author profile 9 10 use crate::html::QuoteRef; 11 use nostr::RelayUrl; 12 use nostrdb::{BlockType, Mention, Ndb, Note, Transaction}; 13 use std::collections::{HashMap, HashSet}; 14 15 /// An unknown ID that needs to be fetched from relays. 16 #[derive(Hash, Eq, PartialEq, Clone, Debug)] 17 pub enum UnknownId { 18 /// A note ID (event) 19 NoteId([u8; 32]), 20 /// A profile pubkey 21 Profile([u8; 32]), 22 } 23 24 /// Collection of unknown IDs with their associated relay hints. 25 #[derive(Default, Debug)] 26 pub struct UnknownIds { 27 ids: HashMap<UnknownId, HashSet<RelayUrl>>, 28 } 29 30 impl UnknownIds { 31 pub fn new() -> Self { 32 Self::default() 33 } 34 35 pub fn is_empty(&self) -> bool { 36 self.ids.is_empty() 37 } 38 39 pub fn ids_len(&self) -> usize { 40 self.ids.len() 41 } 42 43 /// Add a note ID if it's not already in ndb. 44 pub fn add_note_if_missing( 45 &mut self, 46 ndb: &Ndb, 47 txn: &Transaction, 48 id: &[u8; 32], 49 relays: impl IntoIterator<Item = RelayUrl>, 50 ) { 51 // Check if we already have this note 52 if ndb.get_note_by_id(txn, id).is_ok() { 53 return; 54 } 55 56 let unknown_id = UnknownId::NoteId(*id); 57 self.ids.entry(unknown_id).or_default().extend(relays); 58 } 59 60 /// Add a profile pubkey if it's not already in ndb. 61 pub fn add_profile_if_missing(&mut self, ndb: &Ndb, txn: &Transaction, pk: &[u8; 32]) { 62 // Check if we already have this profile 63 if ndb.get_profile_by_pubkey(txn, pk).is_ok() { 64 return; 65 } 66 67 let unknown_id = UnknownId::Profile(*pk); 68 self.ids.entry(unknown_id).or_default(); 69 } 70 71 /// Collect all relay hints from unknowns. 72 pub fn relay_hints(&self) -> HashSet<RelayUrl> { 73 self.ids 74 .values() 75 .flat_map(|relays| relays.iter().cloned()) 76 .collect() 77 } 78 79 /// Build nostrdb filters for fetching unknown IDs. 80 pub fn to_filters(&self) -> Vec<nostrdb::Filter> { 81 if self.ids.is_empty() { 82 return vec![]; 83 } 84 85 let mut filters = Vec::new(); 86 87 // Collect note IDs 88 let note_ids: Vec<&[u8; 32]> = self 89 .ids 90 .keys() 91 .filter_map(|id| match id { 92 UnknownId::NoteId(id) => Some(id), 93 _ => None, 94 }) 95 .collect(); 96 97 if !note_ids.is_empty() { 98 filters.push(nostrdb::Filter::new().ids(note_ids).build()); 99 } 100 101 // Collect profile pubkeys 102 let pubkeys: Vec<&[u8; 32]> = self 103 .ids 104 .keys() 105 .filter_map(|id| match id { 106 UnknownId::Profile(pk) => Some(pk), 107 _ => None, 108 }) 109 .collect(); 110 111 if !pubkeys.is_empty() { 112 filters.push(nostrdb::Filter::new().authors(pubkeys).kinds([0]).build()); 113 } 114 115 filters 116 } 117 118 /// Collect unknown IDs from quote refs. 119 pub fn collect_from_quote_refs( 120 &mut self, 121 ndb: &Ndb, 122 txn: &Transaction, 123 quote_refs: &[QuoteRef], 124 ) { 125 for quote_ref in quote_refs { 126 match quote_ref { 127 QuoteRef::Event { id, relays, .. } => { 128 self.add_note_if_missing(ndb, txn, id, relays.iter().cloned()); 129 } 130 QuoteRef::Article { addr, relays, .. } => { 131 // For articles, we need to parse the address to get the author pubkey 132 // and check if we have the article. For now, just try to look it up. 133 let parts: Vec<&str> = addr.splitn(3, ':').collect(); 134 if parts.len() >= 2 { 135 if let Ok(pk_bytes) = hex::decode(parts[1]) { 136 if let Ok(pk) = pk_bytes.try_into() { 137 // Add author profile if missing 138 self.add_profile_if_missing(ndb, txn, &pk); 139 } 140 } 141 } 142 // Note: For articles we'd ideally build an address filter, 143 // but for now we rely on the profile fetch to help 144 let _ = relays; // TODO: use for article fetching 145 } 146 } 147 } 148 } 149 150 /// Collect all unknown IDs from a note - author, mentioned profiles/events, reply chain. 151 /// 152 /// This is the comprehensive collection function adapted from notedeck's pattern. 153 pub fn collect_from_note(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) { 154 // 1. Author profile 155 self.add_profile_if_missing(ndb, txn, note.pubkey()); 156 157 // 2. Reply chain - check e tags for root/reply markers 158 self.collect_reply_chain(ndb, txn, note); 159 160 // 3. Mentioned profiles and events from content blocks 161 self.collect_from_blocks(ndb, txn, note); 162 } 163 164 /// Collect reply chain unknowns using nostrdb's NoteReply (NIP-10 compliant). 165 fn collect_reply_chain(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) { 166 use nostrdb::NoteReply; 167 168 let reply = NoteReply::new(note.tags()); 169 170 // Add root note if missing 171 if let Some(root_ref) = reply.root() { 172 let relay_hint: Vec<RelayUrl> = root_ref 173 .relay 174 .and_then(|s| RelayUrl::parse(s).ok()) 175 .into_iter() 176 .collect(); 177 self.add_note_if_missing(ndb, txn, root_ref.id, relay_hint); 178 // Also fetch root author profile if root note is already available 179 if let Ok(root_note) = ndb.get_note_by_id(txn, root_ref.id) { 180 self.add_profile_if_missing(ndb, txn, root_note.pubkey()); 181 } 182 } 183 184 // Add reply note if missing (and different from root) 185 if let Some(reply_ref) = reply.reply() { 186 let relay_hint: Vec<RelayUrl> = reply_ref 187 .relay 188 .and_then(|s| RelayUrl::parse(s).ok()) 189 .into_iter() 190 .collect(); 191 self.add_note_if_missing(ndb, txn, reply_ref.id, relay_hint); 192 // Also fetch reply parent author profile if note is already available 193 if let Ok(reply_note) = ndb.get_note_by_id(txn, reply_ref.id) { 194 self.add_profile_if_missing(ndb, txn, reply_note.pubkey()); 195 } 196 } 197 } 198 199 /// Collect unknowns from content blocks (mentions). 200 fn collect_from_blocks(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) { 201 let Some(note_key) = note.key() else { 202 return; 203 }; 204 205 let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) else { 206 return; 207 }; 208 209 for block in blocks.iter(note) { 210 if block.blocktype() != BlockType::MentionBech32 { 211 continue; 212 } 213 214 let Some(mention) = block.as_mention() else { 215 continue; 216 }; 217 218 match mention { 219 // npub - simple pubkey mention 220 Mention::Pubkey(npub) => { 221 self.add_profile_if_missing(ndb, txn, npub.pubkey()); 222 } 223 // nprofile - pubkey with relay hints 224 Mention::Profile(nprofile) => { 225 if ndb.get_profile_by_pubkey(txn, nprofile.pubkey()).is_err() { 226 let relays: HashSet<RelayUrl> = nprofile 227 .relays_iter() 228 .filter_map(|s| RelayUrl::parse(s).ok()) 229 .collect(); 230 let unknown_id = UnknownId::Profile(*nprofile.pubkey()); 231 self.ids.entry(unknown_id).or_default().extend(relays); 232 } 233 } 234 // nevent - event with relay hints 235 Mention::Event(ev) => { 236 let relays: HashSet<RelayUrl> = ev 237 .relays_iter() 238 .filter_map(|s| RelayUrl::parse(s).ok()) 239 .collect(); 240 241 match ndb.get_note_by_id(txn, ev.id()) { 242 Err(_) => { 243 // Event not found - add it and its author if specified 244 self.add_note_if_missing(ndb, txn, ev.id(), relays.clone()); 245 if let Some(pk) = ev.pubkey() { 246 if ndb.get_profile_by_pubkey(txn, pk).is_err() { 247 let unknown_id = UnknownId::Profile(*pk); 248 self.ids.entry(unknown_id).or_default().extend(relays); 249 } 250 } 251 } 252 Ok(found_note) => { 253 // Event found but maybe we need the author profile 254 if ndb.get_profile_by_pubkey(txn, found_note.pubkey()).is_err() { 255 let unknown_id = UnknownId::Profile(*found_note.pubkey()); 256 self.ids.entry(unknown_id).or_default().extend(relays); 257 } 258 } 259 } 260 } 261 // note1 - simple note mention 262 Mention::Note(note_mention) => { 263 match ndb.get_note_by_id(txn, note_mention.id()) { 264 Err(_) => { 265 self.add_note_if_missing( 266 ndb, 267 txn, 268 note_mention.id(), 269 std::iter::empty(), 270 ); 271 } 272 Ok(found_note) => { 273 // Note found but maybe we need the author profile 274 self.add_profile_if_missing(ndb, txn, found_note.pubkey()); 275 } 276 } 277 } 278 _ => {} 279 } 280 } 281 } 282 }