notecrumbs

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

commit e35bb71cc6e777a9c515f6c6e335a4f559bce2a4
parent 2ef85fbc1dd57e67a159698cc3ad5f1fafa85473
Author: William Casarin <jb55@jb55.com>
Date:   Wed,  4 Feb 2026 22:52:34 -0800

refactor: generalize unknowns to fetch all missing data

Expand the unknowns pattern beyond just quoted events to collect:
- Author profiles
- Reply chain (root/reply) using nostrdb's NoteReply (NIP-10)
- Mentioned profiles (npub/nprofile with relay hints)
- Mentioned events (nevent/note1 with relay hints)
- Quoted events (q tags, inline mentions)

Move unknowns collection to main.rs for consistent handling
regardless of whether primary note was cached.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Msrc/main.rs | 10++++------
Msrc/render.rs | 40++++++++++++++--------------------------
Msrc/unknowns.rs | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 145 insertions(+), 34 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -188,16 +188,14 @@ async fn serve( } } - // Always check for quote unknowns when we have a note + // Collect and fetch all unknowns from the note (author, mentions, quotes, replies) if let RenderData::Note(note_rd) = &render_data { - if let Some(unknowns) = - render::collect_quote_unknowns(&app.ndb, &note_rd.note_rd) - { - tracing::debug!("fetching {} quote unknowns", unknowns.relay_hints().len()); + if let Some(unknowns) = render::collect_note_unknowns(&app.ndb, &note_rd.note_rd) { + tracing::debug!("fetching {} unknowns", unknowns.ids_len()); if let Err(err) = render::fetch_unknowns(&app.relay_pool, &app.ndb, unknowns).await { - tracing::warn!("failed to fetch quote unknowns: {err}"); + tracing::warn!("failed to fetch unknowns: {err}"); } } } diff --git a/src/render.rs b/src/render.rs @@ -527,51 +527,39 @@ impl RenderData { } // Wait for primary fetch to complete - let primary_result = match fetch_handle.await { + // Note: unknowns collection happens in main.rs after complete() returns + match fetch_handle.await { Ok(Ok(())) => Ok(()), Ok(Err(err)) => Err(err), Err(join_err) => Err(Error::Generic(format!( "relay fetch task failed: {}", join_err ))), - }; - - // After primary note is fetched, collect and fetch quote unknowns - if let RenderData::Note(note_rd) = self { - debug!("checking for quote unknowns"); - if let Some(unknowns) = collect_quote_unknowns(&ndb, &note_rd.note_rd) { - debug!("fetching {} quote unknowns", unknowns.relay_hints().len()); - if let Err(err) = fetch_unknowns(&relay_pool, &ndb, unknowns).await { - warn!("failed to fetch quote unknowns: {err}"); - } - } else { - debug!("no quote unknowns to fetch"); - } } - - primary_result } } -/// Collect unknown IDs from a note's quote references. -pub fn collect_quote_unknowns( +/// Collect all unknown IDs from a note - author, mentions, quotes, reply chain. +pub fn collect_note_unknowns( ndb: &Ndb, note_rd: &NoteRenderData, ) -> Option<crate::unknowns::UnknownIds> { let txn = Transaction::new(ndb).ok()?; let note = note_rd.lookup(&txn, ndb).ok()?; - let quote_refs = crate::html::collect_all_quote_refs(ndb, &txn, &note); - debug!("found {} quote refs in note", quote_refs.len()); + let mut unknowns = crate::unknowns::UnknownIds::new(); - if quote_refs.is_empty() { - return None; - } + // Collect from note content, author, reply chain, mentioned profiles/events + unknowns.collect_from_note(ndb, &txn, &note); - let mut unknowns = crate::unknowns::UnknownIds::new(); - unknowns.collect_from_quote_refs(ndb, &txn, &quote_refs); + // Also collect from quote refs (q tags and inline nevent/naddr for embedded quotes) + let quote_refs = crate::html::collect_all_quote_refs(ndb, &txn, &note); + if !quote_refs.is_empty() { + debug!("found {} quote refs in note", quote_refs.len()); + unknowns.collect_from_quote_refs(ndb, &txn, &quote_refs); + } - debug!("collected {} unknowns from quote refs", unknowns.ids_len()); + debug!("collected {} total unknowns from note", unknowns.ids_len()); if unknowns.is_empty() { None diff --git a/src/unknowns.rs b/src/unknowns.rs @@ -1,10 +1,15 @@ -//! Unknown ID collection with relay provenance for fetching quoted events. +//! Unknown ID collection with relay provenance for fetching missing data. //! //! Adapted from notedeck's unknowns pattern for notecrumbs' one-shot HTTP context. +//! Collects unknown note IDs and profile pubkeys from: +//! - Quote references (q tags, inline nevent/note/naddr) +//! - Mentioned profiles (npub/nprofile in content) +//! - Reply chain (e tags with reply/root markers) +//! - Author profile use crate::html::QuoteRef; use nostr::RelayUrl; -use nostrdb::{Ndb, Transaction}; +use nostrdb::{BlockType, Mention, Ndb, Note, Transaction}; use std::collections::{HashMap, HashSet}; /// An unknown ID that needs to be fetched from relays. @@ -139,4 +144,124 @@ impl UnknownIds { } } } + + /// Collect all unknown IDs from a note - author, mentioned profiles/events, reply chain. + /// + /// This is the comprehensive collection function adapted from notedeck's pattern. + pub fn collect_from_note(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) { + // 1. Author profile + self.add_profile_if_missing(ndb, txn, note.pubkey()); + + // 2. Reply chain - check e tags for root/reply markers + self.collect_reply_chain(ndb, txn, note); + + // 3. Mentioned profiles and events from content blocks + self.collect_from_blocks(ndb, txn, note); + } + + /// Collect reply chain unknowns using nostrdb's NoteReply (NIP-10 compliant). + fn collect_reply_chain(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) { + use nostrdb::NoteReply; + + let reply = NoteReply::new(note.tags()); + + // Add root note if missing + if let Some(root_ref) = reply.root() { + let relay_hint: Vec<RelayUrl> = root_ref + .relay + .and_then(|s| RelayUrl::parse(s).ok()) + .into_iter() + .collect(); + self.add_note_if_missing(ndb, txn, root_ref.id, relay_hint); + } + + // Add reply note if missing (and different from root) + if let Some(reply_ref) = reply.reply() { + let relay_hint: Vec<RelayUrl> = reply_ref + .relay + .and_then(|s| RelayUrl::parse(s).ok()) + .into_iter() + .collect(); + self.add_note_if_missing(ndb, txn, reply_ref.id, relay_hint); + } + } + + /// Collect unknowns from content blocks (mentions). + fn collect_from_blocks(&mut self, ndb: &Ndb, txn: &Transaction, note: &Note) { + let Some(note_key) = note.key() else { + return; + }; + + let Ok(blocks) = ndb.get_blocks_by_key(txn, note_key) else { + return; + }; + + for block in blocks.iter(note) { + if block.blocktype() != BlockType::MentionBech32 { + continue; + } + + let Some(mention) = block.as_mention() else { + continue; + }; + + match mention { + // npub - simple pubkey mention + Mention::Pubkey(npub) => { + self.add_profile_if_missing(ndb, txn, npub.pubkey()); + } + // nprofile - pubkey with relay hints + Mention::Profile(nprofile) => { + if ndb.get_profile_by_pubkey(txn, nprofile.pubkey()).is_err() { + let relays: HashSet<RelayUrl> = nprofile + .relays_iter() + .filter_map(|s| RelayUrl::parse(s).ok()) + .collect(); + let unknown_id = UnknownId::Profile(*nprofile.pubkey()); + self.ids.entry(unknown_id).or_default().extend(relays); + } + } + // nevent - event with relay hints + Mention::Event(ev) => { + let relays: HashSet<RelayUrl> = ev + .relays_iter() + .filter_map(|s| RelayUrl::parse(s).ok()) + .collect(); + + match ndb.get_note_by_id(txn, ev.id()) { + Err(_) => { + // Event not found - add it and its author if specified + self.add_note_if_missing(ndb, txn, ev.id(), relays.clone()); + if let Some(pk) = ev.pubkey() { + if ndb.get_profile_by_pubkey(txn, pk).is_err() { + let unknown_id = UnknownId::Profile(*pk); + self.ids.entry(unknown_id).or_default().extend(relays); + } + } + } + Ok(found_note) => { + // Event found but maybe we need the author profile + if ndb.get_profile_by_pubkey(txn, found_note.pubkey()).is_err() { + let unknown_id = UnknownId::Profile(*found_note.pubkey()); + self.ids.entry(unknown_id).or_default().extend(relays); + } + } + } + } + // note1 - simple note mention + Mention::Note(note_mention) => { + match ndb.get_note_by_id(txn, note_mention.id()) { + Err(_) => { + self.add_note_if_missing(ndb, txn, note_mention.id(), std::iter::empty()); + } + Ok(found_note) => { + // Note found but maybe we need the author profile + self.add_profile_if_missing(ndb, txn, found_note.pubkey()); + } + } + } + _ => {} + } + } + } }