nostrdb-rs

nostrdb in rust!
git clone git://jb55.com/nostrdb-rs
Log | Files | Refs | Submodules | README | LICENSE

metadata.rs (17683B)


      1 //! Provides structures and methods for handling aggregated note metadata.
      2 //!
      3 //! This module contains the API for reading and building note metadata blobs.
      4 //! Metadata typically includes aggregated data like reaction counts, reply counts,
      5 //! and reposts, which are derived from other events.
      6 //!
      7 //! ## Reading Metadata
      8 //!
      9 //! The primary way to read metadata is to get a [`NoteMetadata`] object
     10 //! from [`Ndb::get_note_metadata`] and then iterate over it.
     11 //!
     12 //! ```no_run
     13 //! # use nostrdb::{Ndb, Transaction, Error, NoteMetadataEntryVariant};
     14 //! # let ndb: Ndb = todo!();
     15 //! # let txn: Transaction = todo!();
     16 //! # let note_id: [u8; 32] = [0; 32];
     17 //! // Get the metadata for a note
     18 //! let metadata = ndb.get_note_metadata(&txn, &note_id).unwrap();
     19 //!
     20 //! // Iterate over the metadata entries
     21 //! for entry in metadata {
     22 //!     match entry {
     23 //!         NoteMetadataEntryVariant::Counts(counts) => {
     24 //!             println!("Total Reactions: {}", counts.reactions());
     25 //!             println!("Thread Replies: {}", counts.thread_replies());
     26 //!         }
     27 //!         NoteMetadataEntryVariant::Reaction(reaction) => {
     28 //!             let mut buf = [0i8; 128];
     29 //!             println!(
     30 //!                 "Reaction: {} (Count: {})",
     31 //!                 reaction.as_str(&mut buf),
     32 //!                 reaction.count()
     33 //!             );
     34 //!         }
     35 //!         NoteMetadataEntryVariant::Unknown(_) => {
     36 //!             // Handle unknown entry types
     37 //!         }
     38 //!     }
     39 //! }
     40 //! ```
     41 //!
     42 //! ## Building Metadata
     43 //!
     44 //! To create a new metadata blob, you can use the [`NoteMetadataBuilder`].
     45 //!
     46 //! ```no_run
     47 //! # use nostrdb::{NoteMetadataBuilder, NoteMetadataEntryBuf, Counts};
     48 //! // Create a "counts" entry
     49 //! let counts_data = Counts {
     50 //!     total_reactions: 10,
     51 //!     thread_replies: 5,
     52 //!     quotes: 2,
     53 //!     direct_replies: 3,
     54 //!     reposts: 1,
     55 //! };
     56 //! let mut counts_entry = NoteMetadataEntryBuf::counts(&counts_data);
     57 //!
     58 //! // Build the metadata blob
     59 //! let mut builder = NoteMetadataBuilder::new();
     60 //! builder.add_entry(counts_entry.borrow());
     61 //! let metadata_buf = builder.build();
     62 //!
     63 //! // The resulting `metadata_buf.buf` (a Vec<u8>) can now be stored.
     64 
     65 use crate::bindings;
     66 
     67 /// A borrowed reference to a note's aggregated metadata.
     68 ///
     69 /// This structure provides read-only access to metadata entries, such as
     70 /// reaction counts, reply counts, etc. It is obtained via
     71 /// [`Ndb::get_note_metadata`].
     72 ///
     73 /// The primary way to use this is by iterating over it, which yields
     74 /// [`NoteMetadataEntryVariant`] items.
     75 pub struct NoteMetadata<'a> {
     76     /// Borrowed, exclusive mutable reference
     77     ptr: &'a bindings::ndb_note_meta,
     78 }
     79 
     80 /// A borrowed reference to a single metadata entry.
     81 ///
     82 /// This is a generic wrapper. It's typically consumed by calling
     83 /// [`.variant()`](Self::variant) to get a specific type, like [`CountsEntry`] or
     84 /// [`ReactionEntry`].
     85 pub struct NoteMetadataEntry<'a> {
     86     entry: &'a bindings::ndb_note_meta_entry,
     87 }
     88 
     89 /// A metadata entry representing aggregated counts for a note.
     90 pub struct CountsEntry<'a> {
     91     entry: NoteMetadataEntry<'a>,
     92 }
     93 
     94 /// A metadata entry representing a specific reaction and its count
     95 /// (e.g., "❤️" - 5 times).
     96 pub struct ReactionEntry<'a> {
     97     entry: NoteMetadataEntry<'a>,
     98 }
     99 
    100 impl<'a> ReactionEntry<'a> {
    101     pub(crate) fn new(entry: NoteMetadataEntry<'a>) -> Self {
    102         Self { entry }
    103     }
    104 
    105     pub fn as_ptr(&self) -> *mut bindings::ndb_note_meta_entry {
    106         self.entry.as_ptr()
    107     }
    108 
    109     /// The number of times this specific reaction was seen.
    110     pub fn count(&self) -> u32 {
    111         unsafe { *bindings::ndb_note_meta_reaction_count(self.as_ptr()) }
    112     }
    113 
    114     /// Gets the string content of the reaction (e.g., "❤️" or "+").
    115     ///
    116     /// Note: This function requires a temporary buffer to write the emoji into.
    117     pub fn as_str(&'a self, buf: &'a mut [i8; 128]) -> &'a str {
    118         unsafe {
    119             let rstr = bindings::ndb_note_meta_reaction_str(self.as_ptr());
    120             // Cast to c_char for platform independence (i8 on Linux, u8 on macOS)
    121             let ptr =
    122                 bindings::ndb_reaction_to_str(rstr, buf.as_mut_ptr() as *mut std::os::raw::c_char);
    123             let byte_slice: &[u8] = std::slice::from_raw_parts(ptr as *mut u8, libc::strlen(ptr));
    124             std::str::from_utf8_unchecked(byte_slice)
    125         }
    126     }
    127 }
    128 
    129 impl<'a> CountsEntry<'a> {
    130     pub(crate) fn new(entry: NoteMetadataEntry<'a>) -> Self {
    131         Self { entry }
    132     }
    133 
    134     pub fn as_ptr(&self) -> *mut bindings::ndb_note_meta_entry {
    135         self.entry.as_ptr()
    136     }
    137 
    138     /// Total number of replies in the thread (recursive).
    139     pub fn thread_replies(&self) -> u32 {
    140         unsafe { *bindings::ndb_note_meta_counts_thread_replies(self.as_ptr()) }
    141     }
    142 
    143     /// Number of direct replies to the note.
    144     pub fn direct_replies(&self) -> u16 {
    145         unsafe { *bindings::ndb_note_meta_counts_direct_replies(self.as_ptr()) }
    146     }
    147 
    148     /// Number of quotes (reposts with content).
    149     pub fn quotes(&self) -> u16 {
    150         unsafe { *bindings::ndb_note_meta_counts_quotes(self.as_ptr()) }
    151     }
    152 
    153     /// Number of simple reposts (kind 6/16).
    154     pub fn reposts(&self) -> u16 {
    155         unsafe { *bindings::ndb_note_meta_counts_reposts(self.as_ptr()) }
    156     }
    157 
    158     /// Total number of reactions (e.g., kind 7) of all types.
    159     pub fn reactions(&self) -> u32 {
    160         unsafe { *bindings::ndb_note_meta_counts_total_reactions(self.as_ptr()) }
    161     }
    162 }
    163 
    164 /// An enumeration of the different types of note metadata entries.
    165 ///
    166 /// This is the item yielded when iterating over [`NoteMetadata`].
    167 pub enum NoteMetadataEntryVariant<'a> {
    168     /// Aggregated counts (replies, reposts, reactions).
    169     Counts(CountsEntry<'a>),
    170 
    171     /// A specific reaction (e.g., "❤️") and its count.
    172     Reaction(ReactionEntry<'a>),
    173 
    174     /// An entry of an unknown or unsupported type.
    175     Unknown(NoteMetadataEntry<'a>),
    176 }
    177 
    178 impl<'a> NoteMetadataEntryVariant<'a> {
    179     pub fn new(entry: NoteMetadataEntry<'a>) -> Self {
    180         if entry.type_id() == bindings::ndb_metadata_type_NDB_NOTE_META_COUNTS as u16 {
    181             NoteMetadataEntryVariant::Counts(CountsEntry::new(entry))
    182         } else if entry.type_id() == bindings::ndb_metadata_type_NDB_NOTE_META_REACTION as u16 {
    183             NoteMetadataEntryVariant::Reaction(ReactionEntry::new(entry))
    184         } else {
    185             NoteMetadataEntryVariant::Unknown(entry)
    186         }
    187     }
    188 }
    189 
    190 /// An owned buffer representing a single metadata entry.
    191 ///
    192 /// This is used with the [`NoteMetadataBuilder`] to construct a complete
    193 /// metadata blob.
    194 pub struct NoteMetadataEntryBuf {
    195     entry: bindings::ndb_note_meta_entry,
    196 }
    197 
    198 /// A plain data struct used to create a "Counts" metadata entry.
    199 ///
    200 /// See [`NoteMetadataEntryBuf::counts`].
    201 pub struct Counts {
    202     pub total_reactions: u32,
    203     pub thread_replies: u32,
    204     pub quotes: u16,
    205     pub direct_replies: u16,
    206     pub reposts: u16,
    207 }
    208 
    209 impl<'a> NoteMetadataEntry<'a> {
    210     pub fn new(entry: &'a bindings::ndb_note_meta_entry) -> Self {
    211         Self { entry }
    212     }
    213 
    214     pub fn entry(&self) -> &bindings::ndb_note_meta_entry {
    215         self.entry
    216     }
    217 
    218     pub fn as_ptr(&self) -> *mut bindings::ndb_note_meta_entry {
    219         self.entry() as *const bindings::ndb_note_meta_entry as *mut bindings::ndb_note_meta_entry
    220     }
    221 
    222     pub fn type_id(&self) -> u16 {
    223         unsafe { *bindings::ndb_note_meta_entry_type(self.as_ptr()) }
    224     }
    225 
    226     pub fn variant(self) -> NoteMetadataEntryVariant<'a> {
    227         NoteMetadataEntryVariant::new(self)
    228     }
    229 }
    230 
    231 /// An iterator over metadata entries in a [`NoteMetadata`] object.
    232 pub struct NoteMetadataEntryIter<'a> {
    233     metadata: NoteMetadata<'a>,
    234     index: u16,
    235 }
    236 
    237 impl<'a> NoteMetadataEntryIter<'a> {
    238     pub fn new(metadata: NoteMetadata<'a>) -> Self {
    239         Self { index: 0, metadata }
    240     }
    241 
    242     pub fn done(&mut self) -> bool {
    243         self.index >= self.metadata.count()
    244     }
    245 }
    246 
    247 impl<'a> Iterator for NoteMetadataEntryIter<'a> {
    248     type Item = NoteMetadataEntryVariant<'a>;
    249 
    250     fn next(&mut self) -> Option<NoteMetadataEntryVariant<'a>> {
    251         if self.done() {
    252             return None;
    253         }
    254 
    255         let ind = self.index;
    256         self.index += 1;
    257 
    258         self.metadata.entry_at(ind).map(|e| e.variant())
    259     }
    260 }
    261 
    262 impl<'a> IntoIterator for NoteMetadata<'a> {
    263     type Item = NoteMetadataEntryVariant<'a>;
    264     type IntoIter = NoteMetadataEntryIter<'a>;
    265 
    266     fn into_iter(self) -> Self::IntoIter {
    267         NoteMetadataEntryIter::new(self)
    268     }
    269 }
    270 
    271 impl bindings::ndb_note_meta_builder {
    272     pub fn as_mut_ptr(&mut self) -> *mut bindings::ndb_note_meta_builder {
    273         self as *mut bindings::ndb_note_meta_builder
    274     }
    275 }
    276 
    277 impl NoteMetadataEntryBuf {
    278     pub fn counts(counts: &Counts) -> Self {
    279         let mut me = Self {
    280             entry: bindings::ndb_note_meta_entry {
    281                 type_: 0,
    282                 aux: bindings::ndb_note_meta_entry__bindgen_ty_2 { value: 0 },
    283                 aux2: bindings::ndb_note_meta_entry__bindgen_ty_1 { reposts: 0 },
    284                 payload: bindings::ndb_note_meta_entry__bindgen_ty_3 { value: 0 },
    285             },
    286         };
    287 
    288         unsafe {
    289             bindings::ndb_note_meta_counts_set(
    290                 me.as_ptr(),
    291                 counts.total_reactions,
    292                 counts.quotes,
    293                 counts.direct_replies,
    294                 counts.thread_replies,
    295                 counts.reposts,
    296             );
    297         };
    298 
    299         me
    300     }
    301 
    302     pub fn as_ptr(&mut self) -> *mut bindings::ndb_note_meta_entry {
    303         self.borrow().as_ptr()
    304     }
    305 
    306     pub fn borrow<'a>(&'a mut self) -> NoteMetadataEntry<'a> {
    307         NoteMetadataEntry {
    308             entry: &mut self.entry,
    309         }
    310     }
    311 }
    312 
    313 impl bindings::ndb_note_meta {
    314     pub fn as_mut_ptr(&mut self) -> *mut bindings::ndb_note_meta {
    315         self as *mut bindings::ndb_note_meta
    316     }
    317 }
    318 
    319 /// An owned, heap-allocated buffer containing a complete note metadata blob.
    320 ///
    321 /// This is the output of the [`NoteMetadataBuilder`]. The internal `buf` can be
    322 /// used to write the metadata to the database.
    323 pub struct NoteMetadataBuf {
    324     pub buf: Vec<u8>,
    325 }
    326 
    327 /// A builder for constructing a new [`NoteMetadataBuf`].
    328 ///
    329 /// This is used to create the raw metadata blob that can be stored in the database.
    330 /// See the [module-level documentation](self) for a build example.
    331 pub struct NoteMetadataBuilder {
    332     buf: Vec<u8>,
    333     builder: bindings::ndb_note_meta_builder,
    334 }
    335 
    336 impl Default for NoteMetadataBuilder {
    337     fn default() -> Self {
    338         Self::new()
    339     }
    340 }
    341 
    342 impl NoteMetadataBuilder {
    343     /// Creates a new builder with a default initial capacity.
    344     pub fn new() -> Self {
    345         Self::with_capacity(128)
    346     }
    347 
    348     /// Finalizes the build and returns an owned [`NoteMetadataBuf`].
    349     pub fn build(mut self) -> NoteMetadataBuf {
    350         let size = unsafe {
    351             let mut meta: *mut bindings::ndb_note_meta = std::ptr::null_mut();
    352             bindings::ndb_note_meta_build(self.builder.as_mut_ptr(), &mut meta);
    353             assert!(!meta.is_null());
    354             bindings::ndb_note_meta_total_size(meta)
    355         };
    356         if size < self.buf.capacity() {
    357             self.buf.truncate(size);
    358         }
    359         unsafe {
    360             self.buf.set_len(size);
    361         }
    362         NoteMetadataBuf { buf: self.buf }
    363     }
    364 
    365     /// Adds a metadata entry to the builder.
    366     ///
    367     /// This may reallocate the internal buffer if more space is needed.
    368     pub fn add_entry(&mut self, entry: NoteMetadataEntry<'_>) {
    369         let remaining = self.buf.capacity() - self.buf.len();
    370         if remaining < 16 {
    371             self.buf.reserve(16);
    372             unsafe {
    373                 bindings::ndb_note_meta_builder_resized(
    374                     self.builder.as_mut_ptr(),
    375                     self.buf.as_mut_ptr(),
    376                     self.buf.capacity(),
    377                 );
    378             }
    379         }
    380         unsafe {
    381             let entry_ptr = bindings::ndb_note_meta_add_entry(self.builder.as_mut_ptr());
    382             if entry_ptr.is_null() {
    383                 panic!("out of memory?");
    384             }
    385             self.buf.set_len(self.buf.len() + 16);
    386             libc::memcpy(
    387                 entry_ptr as *mut std::ffi::c_void,
    388                 entry.as_ptr() as *const std::ffi::c_void,
    389                 16,
    390             );
    391         }
    392     }
    393 
    394     /// Creates a new builder with a specific capacity (in number of entries).
    395     pub fn with_capacity(capacity: usize) -> Self {
    396         let size = 16 * capacity;
    397         let mut me = Self {
    398             buf: Vec::with_capacity(size),
    399             builder: bindings::ndb_note_meta_builder {
    400                 cursor: bindings::cursor {
    401                     start: std::ptr::null_mut(),
    402                     p: std::ptr::null_mut(),
    403                     end: std::ptr::null_mut(),
    404                 },
    405             },
    406         };
    407 
    408         unsafe {
    409             bindings::ndb_note_meta_builder_init(
    410                 me.builder.as_mut_ptr(),
    411                 me.buf.as_mut_ptr(),
    412                 size,
    413             );
    414         };
    415 
    416         me
    417     }
    418 }
    419 
    420 impl<'a> NoteMetadata<'a> {
    421     pub fn new(ptr: &'a bindings::ndb_note_meta) -> NoteMetadata<'a> {
    422         Self { ptr }
    423     }
    424 
    425     #[inline]
    426     pub fn as_ptr(&self) -> *mut bindings::ndb_note_meta {
    427         self.ptr as *const bindings::ndb_note_meta as *mut bindings::ndb_note_meta
    428     }
    429 
    430     pub fn count(&self) -> u16 {
    431         unsafe { bindings::ndb_note_meta_entries_count(self.as_ptr()) }
    432     }
    433 
    434     pub fn entry_at(&self, index: u16) -> Option<NoteMetadataEntry<'a>> {
    435         if index > self.count() - 1 {
    436             return None;
    437         }
    438 
    439         let ptr = unsafe {
    440             bindings::ndb_note_meta_entry_at(self.as_ptr(), index as std::os::raw::c_int)
    441         };
    442 
    443         Some(NoteMetadataEntry::new(unsafe { &mut *ptr }))
    444     }
    445 
    446     pub fn flags(&mut self) -> &mut u64 {
    447         unsafe {
    448             let p = bindings::ndb_note_meta_flags(self.as_ptr());
    449             &mut *p
    450         }
    451     }
    452 }
    453 
    454 #[cfg(test)]
    455 mod tests {
    456     use super::*;
    457     use crate::config::Config;
    458     use crate::test_util;
    459     use crate::{Filter, Ndb, NoteKey, Transaction};
    460     use futures::StreamExt;
    461 
    462     #[tokio::test]
    463     async fn test_metadata() {
    464         let db = "target/testdbs/test_metadata";
    465         test_util::cleanup_db(&db);
    466 
    467         {
    468             let mut ndb = Ndb::new(db, &Config::new()).expect("ndb");
    469             let filter = Filter::new().kinds(vec![7]).build();
    470             let filters = vec![filter];
    471 
    472             let sub_id = ndb.subscribe(&filters).expect("sub_id");
    473             let mut sub = sub_id.stream(&ndb).notes_per_await(1);
    474             let id: [u8; 32] = [
    475                 0xd4, 0x4a, 0xd9, 0x6c, 0xb8, 0x92, 0x40, 0x92, 0xa7, 0x6b, 0xc2, 0xaf, 0xdd, 0xeb,
    476                 0x12, 0xeb, 0x85, 0x23, 0x3c, 0x0d, 0x03, 0xa7, 0xd9, 0xad, 0xc4, 0x2c, 0x2a, 0x85,
    477                 0xa7, 0x9a, 0x43, 0x05,
    478             ];
    479 
    480             let _ = ndb.process_event(r#"["EVENT","a",{"content":"👀","created_at":1761514455,"id":"66af95a6bdfec756344f48241562b684082ff9c76ea940c11c4fd85e91e1219c","kind":7,"pubkey":"d5805ae449e108e907091c67cdf49a9835b3cac3dd11489ad215c0ddf7c658fc","sig":"69f4a3fe7c1cc6aa9c9cc4a2e90e4b71c3b9afaad262e68b92336e0493ff1a748b5dcc20ab6e86d4551dc5ea680ddfa1c08d47f9e4845927e143e8ef2183479b","tags":[["e","d44ad96cb8924092a76bc2afddeb12eb85233c0d03a7d9adc42c2a85a79a4305","wss://relay.primal.net/","04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9"],["p","04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9","wss://relay.primal.net/"],["k","1"]]}]"#);
    481 
    482             let _ = ndb.process_event(r#"["EVENT","b",{"content":"+","created_at":1761514412,"id":"7124bca1479edeb1476d94ed6620ee1210194590b08cf1df385d053679d73fe7","kind":7,"pubkey":"af92154b4fd002924031386f71333b0afd9741a076f5c738bc2603a5b59d671f","sig":"311e7b92ae479262c8ad91ee745eca9c78d469459577d7fb598bff1e6c580f289b3c1d82cd769d0891da9248250d6877357ddaf293f33f496af9e6c8894bc485","tags":[["p","04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9","wss://premium.primal.net/","ODELL"],["k","1"],["e","d44ad96cb8924092a76bc2afddeb12eb85233c0d03a7d9adc42c2a85a79a4305","wss://premium.primal.net/"],["client","Coracle","31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1685968093690"]]}]"#);
    483 
    484             let res = sub.next().await.expect("await ok");
    485             assert_eq!(res, vec![NoteKey::new(1)]);
    486 
    487             sub.next().await.expect("await ok");
    488             //assert_eq!(res, vec![NoteKey::new(2)]);
    489 
    490             // ensure that unsubscribing kills the stream
    491             assert!(ndb.unsubscribe(sub_id).is_ok());
    492             assert!(sub.next().await.is_none());
    493 
    494             let txn = Transaction::new(&ndb).unwrap();
    495             let meta = ndb.get_note_metadata(&txn, &id).expect("what");
    496             let mut count = 0;
    497             let mut buf: [i8; 128] = [0; 128];
    498 
    499             for entry in meta {
    500                 match entry {
    501                     NoteMetadataEntryVariant::Counts(counts) => {
    502                         assert!(counts.reactions() == 2)
    503                     }
    504 
    505                     NoteMetadataEntryVariant::Reaction(reaction) => {
    506                         let s = reaction.as_str(&mut buf);
    507                         assert!(s == "👀" || s == "+");
    508                         assert!(reaction.count() == 1);
    509                     }
    510 
    511                     NoteMetadataEntryVariant::Unknown(_) => {
    512                         assert!(false);
    513                     }
    514                 }
    515                 count += 1;
    516             }
    517 
    518             // 1 count entry, 2 reaction entries
    519             assert!(count == 3);
    520         }
    521 
    522         test_util::cleanup_db(&db);
    523     }
    524 }