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, ¬e_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 }