notecrumbs

a nostr opengraph server build on nostrdb and egui
git clone git://jb55.com/notecrumbs
Log | Files | Refs | README | LICENSE

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 }