notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 8399c951faea0937c4317d2f5df1daca60f77827
parent ac1bbeac1b98a567521b49c67b264873718d077e
Author: kernelkind <kernelkind@gmail.com>
Date:   Thu,  7 Aug 2025 17:13:13 -0400

add nip51 set caching structs

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/lib.rs | 2++
Acrates/notedeck/src/nip51_set.rs | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 197 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -16,6 +16,7 @@ mod jobs; pub mod media; mod muted; pub mod name; +mod nip51_set; pub mod note; mod notecache; mod options; @@ -65,6 +66,7 @@ pub use media::{ }; pub use muted::{MuteFun, Muted}; pub use name::NostrName; +pub use nip51_set::{create_nip51_set, Nip51Set, Nip51SetCache}; pub use note::{ BroadcastContext, ContextSelection, NoteAction, NoteContext, NoteContextSelection, NoteRef, RootIdError, RootNoteId, RootNoteIdBuf, ScrollInfo, ZapAction, diff --git a/crates/notedeck/src/nip51_set.rs b/crates/notedeck/src/nip51_set.rs @@ -0,0 +1,195 @@ +use std::collections::HashMap; + +use enostr::{Pubkey, RelayPool}; +use nostrdb::{Filter, Ndb, Note, Transaction}; +use uuid::Uuid; + +use crate::{UnifiedSubscription, UnknownIds}; + +/// Keeps track of most recent NIP-51 sets +#[derive(Debug)] +pub struct Nip51SetCache { + pub sub: UnifiedSubscription, + cached_notes: HashMap<PackId, Nip51Set>, +} + +type PackId = String; + +impl Nip51SetCache { + pub fn new( + pool: &mut RelayPool, + ndb: &Ndb, + txn: &Transaction, + unknown_ids: &mut UnknownIds, + nip51_set_filter: Vec<Filter>, + ) -> Option<Self> { + let subid = Uuid::new_v4().to_string(); + let mut cached_notes = HashMap::default(); + + let notes: Option<Vec<Note>> = if let Ok(results) = ndb.query(txn, &nip51_set_filter, 500) { + Some(results.into_iter().map(|r| r.note).collect()) + } else { + None + }; + + if let Some(notes) = notes { + add(notes, &mut cached_notes, ndb, txn, unknown_ids); + } + + let sub = match ndb.subscribe(&nip51_set_filter) { + Ok(sub) => sub, + Err(e) => { + tracing::error!("Could not ndb subscribe: {e}"); + return None; + } + }; + pool.subscribe(subid.clone(), nip51_set_filter); + + Some(Self { + sub: UnifiedSubscription { + local: sub, + remote: subid, + }, + cached_notes, + }) + } + + pub fn poll_for_notes(&mut self, ndb: &Ndb, unknown_ids: &mut UnknownIds) { + let new_notes = ndb.poll_for_notes(self.sub.local, 5); + + if new_notes.is_empty() { + return; + } + + let txn = Transaction::new(ndb).expect("txn"); + let notes: Vec<Note> = new_notes + .into_iter() + .filter_map(|new_note_key| ndb.get_note_by_key(&txn, new_note_key).ok()) + .collect(); + + add(notes, &mut self.cached_notes, ndb, &txn, unknown_ids); + } + + pub fn iter(&self) -> impl IntoIterator<Item = &Nip51Set> { + self.cached_notes.values() + } +} + +fn add( + notes: Vec<Note>, + cache: &mut HashMap<PackId, Nip51Set>, + ndb: &Ndb, + txn: &Transaction, + unknown_ids: &mut UnknownIds, +) { + for note in notes { + let Some(new_pack) = create_nip51_set(note) else { + continue; + }; + + if let Some(cur_cached) = cache.get(&new_pack.identifier) { + if new_pack.created_at <= cur_cached.created_at { + continue; + } + } + + for pk in &new_pack.pks { + unknown_ids.add_pubkey_if_missing(ndb, txn, pk); + } + + cache.insert(new_pack.identifier.clone(), new_pack); + } +} + +pub fn create_nip51_set(note: Note) -> Option<Nip51Set> { + let mut identifier = None; + let mut title = None; + let mut image = None; + let mut description = None; + let mut pks = Vec::new(); + + for tag in note.tags() { + if tag.count() < 2 { + continue; + } + + let Some(first) = tag.get_str(0) else { + continue; + }; + + match first { + "p" => { + let Some(pk) = tag.get_id(1) else { + continue; + }; + + pks.push(Pubkey::new(*pk)); + } + "d" => { + let Some(id) = tag.get_str(1) else { + continue; + }; + + identifier = Some(id.to_owned()); + } + "image" => { + let Some(cur_img) = tag.get_str(1) else { + continue; + }; + + image = Some(cur_img.to_owned()); + } + "title" => { + let Some(cur_title) = tag.get_str(1) else { + continue; + }; + + title = Some(cur_title.to_owned()); + } + "description" => { + let Some(cur_desc) = tag.get_str(1) else { + continue; + }; + + description = Some(cur_desc.to_owned()); + } + _ => { + continue; + } + }; + } + + let identifier = identifier?; + + Some(Nip51Set { + identifier, + title, + image, + description, + pks, + created_at: note.created_at(), + }) +} + +/// NIP-51 Set. Read only (do not use for writing) +pub struct Nip51Set { + pub identifier: String, // 'd' tag + pub title: Option<String>, + pub image: Option<String>, + pub description: Option<String>, + pub pks: Vec<Pubkey>, + created_at: u64, +} + +impl std::fmt::Debug for Nip51Set { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Nip51Set") + .field("identifier", &self.identifier) + .field("title", &self.title) + .field("image", &self.image) + .field("description", &self.description) + .field("pks", &self.pks.len()) + .field("created_at", &self.created_at) + .finish() + } +}