use crate::{bindings, Note, Transaction};
#[derive(Debug)]
pub struct Blocks<'a> {
ptr: *mut bindings::ndb_blocks,
txn: Option<&'a Transaction>,
}
#[derive(Debug)]
pub struct Block<'a> {
ptr: *mut bindings::ndb_block,
#[allow(dead_code)]
txn: Option<&'a Transaction>,
}
pub struct BlockIter<'a> {
iter: bindings::ndb_block_iterator,
txn: Option<&'a Transaction>,
}
#[derive(Debug, Eq, PartialEq)]
pub enum BlockType {
Hashtag,
Text,
MentionIndex,
MentionBech32,
Url,
Invoice,
}
#[derive(Debug, Eq, PartialEq)]
pub enum Bech32Type {
Event,
Pubkey,
Profile,
Note,
Relay,
Addr,
Secret,
}
pub enum Mention<'a> {
Pubkey(&'a bindings::bech32_npub),
Event(&'a bindings::bech32_nevent),
Profile(&'a bindings::bech32_nprofile),
Note(&'a bindings::bech32_note),
Relay(&'a bindings::bech32_nrelay),
Secret(&'a bindings::bech32_nsec),
Addr(&'a bindings::bech32_naddr),
}
impl bindings::ndb_str_block {
pub fn as_str(&self) -> &str {
unsafe {
let ptr = bindings::ndb_str_block_ptr(self as *const Self as *mut Self) as *const u8;
let len = bindings::ndb_str_block_len(self as *const Self as *mut Self);
let byte_slice = std::slice::from_raw_parts(ptr, len.try_into().unwrap());
std::str::from_utf8_unchecked(byte_slice)
}
}
}
impl bindings::bech32_nrelay {
pub fn as_str(&self) -> &str {
self.relay.as_str()
}
}
impl bindings::bech32_nprofile {
pub fn pubkey(&self) -> &[u8; 32] {
unsafe { &*(self.pubkey as *const [u8; 32]) }
}
pub fn relays_iter(&self) -> impl Iterator<Item = &str> {
self.relays.relays[0..(self.relays.num_relays as usize)]
.iter()
.map(|block| block.as_str())
}
}
impl bindings::bech32_npub {
pub fn pubkey(&self) -> &[u8; 32] {
unsafe { &*(self.pubkey as *const [u8; 32]) }
}
}
impl bindings::bech32_note {
pub fn id(&self) -> &[u8; 32] {
unsafe { &*(self.event_id as *const [u8; 32]) }
}
}
impl bindings::bech32_nevent {
pub fn id(&self) -> &[u8; 32] {
unsafe { &*(self.event_id as *const [u8; 32]) }
}
pub fn pubkey(&self) -> Option<&[u8; 32]> {
unsafe {
if self.pubkey.is_null() {
return None;
}
Some(&*(self.pubkey as *const [u8; 32]))
}
}
pub fn relays_iter(&self) -> impl Iterator<Item = &str> {
self.relays.relays[0..(self.relays.num_relays as usize)]
.iter()
.map(|block| block.as_str())
}
}
impl bindings::bech32_naddr {
pub fn relays_iter(&self) -> impl Iterator<Item = &str> {
self.relays.relays[0..(self.relays.num_relays as usize)]
.iter()
.map(|block| block.as_str())
}
}
impl<'a> Mention<'a> {
pub fn new(bech32: &'a bindings::nostr_bech32) -> Self {
unsafe {
match Bech32Type::from_ctype(bech32.type_) {
Bech32Type::Event => Mention::Event(&bech32.__bindgen_anon_1.nevent),
Bech32Type::Pubkey => Mention::Pubkey(&bech32.__bindgen_anon_1.npub),
Bech32Type::Profile => Mention::Profile(&bech32.__bindgen_anon_1.nprofile),
Bech32Type::Note => Mention::Note(&bech32.__bindgen_anon_1.note),
Bech32Type::Relay => Mention::Relay(&bech32.__bindgen_anon_1.nrelay),
Bech32Type::Secret => Mention::Secret(&bech32.__bindgen_anon_1.nsec),
Bech32Type::Addr => Mention::Addr(&bech32.__bindgen_anon_1.naddr),
}
}
}
}
impl Bech32Type {
pub(crate) fn from_ctype(typ: bindings::nostr_bech32_type) -> Bech32Type {
match typ {
1 => Bech32Type::Note,
2 => Bech32Type::Pubkey,
3 => Bech32Type::Profile,
4 => Bech32Type::Event,
5 => Bech32Type::Relay,
6 => Bech32Type::Addr,
7 => Bech32Type::Secret,
_ => panic!("Invalid bech32 type"),
}
}
}
impl<'a> Block<'a> {
#[allow(dead_code)]
pub(crate) fn new_transactional(
ptr: *mut bindings::ndb_block,
txn: &'a Transaction,
) -> Block<'a> {
Block {
ptr,
txn: Some(txn),
}
}
#[allow(dead_code)]
pub(crate) fn new_owned(ptr: *mut bindings::ndb_block) -> Block<'static> {
Block { ptr, txn: None }
}
pub(crate) fn new(ptr: *mut bindings::ndb_block, txn: Option<&'a Transaction>) -> Block<'a> {
Block { ptr, txn }
}
pub fn as_ptr(&self) -> *mut bindings::ndb_block {
self.ptr
}
pub fn as_mention(&self) -> Option<Mention<'a>> {
if self.blocktype() != BlockType::MentionBech32 {
return None;
}
Some(Mention::new(self.c_bech32()))
}
pub fn as_str(&self) -> &'a str {
unsafe {
let str_block = bindings::ndb_block_str(self.as_ptr());
if str_block.is_null() {
return "";
}
(*str_block).as_str()
}
}
fn c_bech32(&self) -> &'a bindings::nostr_bech32 {
unsafe { &(*self.as_ptr()).block.mention_bech32.bech32 }
}
pub fn blocktype(&self) -> BlockType {
let typ = unsafe { bindings::ndb_get_block_type(self.as_ptr()) };
match typ {
1 => BlockType::Hashtag,
2 => BlockType::Text,
3 => BlockType::MentionIndex,
4 => BlockType::MentionBech32,
5 => BlockType::Url,
6 => BlockType::Invoice,
_ => panic!("Invalid blocktype {typ}"),
}
}
}
impl<'a> Blocks<'a> {
pub(crate) fn new_transactional(
ptr: *mut bindings::ndb_blocks,
txn: &'a Transaction,
) -> Blocks<'a> {
Blocks {
ptr,
txn: Some(txn),
}
}
#[allow(dead_code)]
pub(crate) fn new_owned(ptr: *mut bindings::ndb_blocks) -> Blocks<'static> {
Blocks { ptr, txn: None }
}
pub fn iter(&self, note: &Note<'a>) -> BlockIter<'a> {
let content = note.content_ptr();
match self.txn {
Some(txn) => BlockIter::new_transactional(content, self.as_ptr(), txn),
None => BlockIter::new_owned(content, self.as_ptr()),
}
}
pub fn as_ptr(&self) -> *mut bindings::ndb_blocks {
self.ptr
}
}
impl Drop for Blocks<'_> {
fn drop(&mut self) {
unsafe { bindings::ndb_blocks_free(self.as_ptr()) };
}
}
impl<'a> BlockIter<'a> {
pub(crate) fn new_transactional(
content: *const ::std::os::raw::c_char,
blocks: *mut bindings::ndb_blocks,
txn: &'a Transaction,
) -> BlockIter<'a> {
let type_ = bindings::ndb_block_type_BLOCK_TEXT;
let mention_index: u32 = 1;
let block = bindings::ndb_block__bindgen_ty_1 { mention_index };
let block = bindings::ndb_block { type_, block };
let p = blocks as *mut ::std::os::raw::c_uchar;
let iter = bindings::ndb_block_iterator {
content,
blocks,
p,
block,
};
let mut block_iter = BlockIter {
iter,
txn: Some(txn),
};
unsafe { bindings::ndb_blocks_iterate_start(content, blocks, &mut block_iter.iter) };
block_iter
}
pub(crate) fn new_owned(
content: *const ::std::os::raw::c_char,
blocks: *mut bindings::ndb_blocks,
) -> BlockIter<'static> {
let type_ = bindings::ndb_block_type_BLOCK_TEXT;
let mention_index: u32 = 1;
let block = bindings::ndb_block__bindgen_ty_1 { mention_index };
let block = bindings::ndb_block { type_, block };
let p = blocks as *mut ::std::os::raw::c_uchar;
let mut iter = bindings::ndb_block_iterator {
content,
blocks,
p,
block,
};
unsafe { bindings::ndb_blocks_iterate_start(content, blocks, &mut iter) };
BlockIter { iter, txn: None }
}
pub fn as_ptr(&self) -> *const bindings::ndb_block_iterator {
&self.iter
}
pub fn as_mut_ptr(&self) -> *mut bindings::ndb_block_iterator {
self.as_ptr() as *mut bindings::ndb_block_iterator
}
}
impl<'a> Iterator for BlockIter<'a> {
type Item = Block<'a>;
fn next(&mut self) -> Option<Self::Item> {
let block = unsafe { bindings::ndb_blocks_iterate_next(self.as_mut_ptr()) };
if block.is_null() {
return None;
}
Some(Block::new(block, self.txn))
}
}
/*
impl<'a> IntoIterator for Blocks<'a> {
type Item = Block<'a>;
type IntoIter = BlockIter<'a>;
fn into_iter(self) -> Self::IntoIter {
match self.txn {
Some(txn) => BlockIter::new_transactional(self.as_ptr(), txn),
None => BlockIter::new_owned(self.as_ptr()),
}
}
}
*/
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util;
use crate::{Config, Ndb};
#[test]
fn note_blocks_work() {
let db = "target/testdbs/note_blocks";
test_util::cleanup_db(&db);
{
let ndb = Ndb::new(db, &Config::new()).expect("ndb");
ndb.process_event("[\"EVENT\",\"s\",{\"id\":\"d28ac02e277c3cf2744b562a414fd92d5fea554a737901364735bfe74577f304\",\"pubkey\":\"b5b1b5d2914daa2eda99af22ae828effe98730bf69dcca000fa37bfb9e395e32\",\"created_at\": 1703989205,\"kind\": 1,\"tags\": [],\"content\": \"#hashtags, are neat nostr:nprofile1qqsr9cvzwc652r4m83d86ykplrnm9dg5gwdvzzn8ameanlvut35wy3gpz3mhxue69uhhyetvv9ujuerpd46hxtnfduyu75sw https://github.com/damus-io\",\"sig\": \"07af3062616a17ef392769cadb170ac855c817c103e007c72374499bbadb2fe8917a0cc5b3fdc5aa5d56de086e128b3aeaa8868f6fe42a409767241b6a29cc94\"}]").expect("process ok");
}
{
let ndb = Ndb::new(db, &Config::new()).expect("ndb");
let id =
hex::decode("d28ac02e277c3cf2744b562a414fd92d5fea554a737901364735bfe74577f304")
.expect("hex id");
let pubkey =
hex::decode("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")
.expect("jb55 pubkey");
let txn = Transaction::new(&ndb).expect("txn");
let id_bytes: [u8; 32] = id.try_into().expect("id bytes");
let pubkey_bytes: [u8; 32] = pubkey.try_into().expect("pubkey bytes");
let note = ndb.get_note_by_id(&txn, &id_bytes).unwrap();
let blocks = ndb
.get_blocks_by_key(&txn, note.key().unwrap())
.expect("note");
let mut c = 0;
for block in blocks.iter(¬e) {
match c {
0 => {
assert_eq!(block.blocktype(), BlockType::Hashtag);
assert_eq!(block.as_str(), "hashtags");
}
1 => {
assert_eq!(block.blocktype(), BlockType::Text);
assert_eq!(block.as_str(), ", are neat ");
}
2 => {
assert_eq!(block.blocktype(), BlockType::MentionBech32);
assert_eq!(block.as_str(), "nprofile1qqsr9cvzwc652r4m83d86ykplrnm9dg5gwdvzzn8ameanlvut35wy3gpz3mhxue69uhhyetvv9ujuerpd46hxtnfduyu75sw");
match block.as_mention().unwrap() {
Mention::Profile(p) => assert_eq!(p.pubkey(), &pubkey_bytes),
_ => assert!(false),
};
}
3 => {
assert_eq!(block.blocktype(), BlockType::Text);
assert_eq!(block.as_str(), " ");
}
4 => {
assert_eq!(block.blocktype(), BlockType::Url);
assert_eq!(block.as_str(), "https://github.com/damus-io");
}
_ => assert!(false),
}
c += 1;
}
}
test_util::cleanup_db(&db);
}
#[test]
fn nprofile_relays_work() {
let db = "target/testdbs/nprofile_relays";
test_util::cleanup_db(&db);
{
let ndb = Ndb::new(db, &Config::new()).expect("ndb");
ndb.process_event("[\"EVENT\",\"s\",{\"kind\":1,\"id\":\"06449981af8f0e00503ea88deb6c2d3e9d65e1e4f032744d9ca4949dce1ce296\",\"pubkey\":\"850605096dbfb50b929e38a6c26c3d56c425325c85e05de29b759bc0e5d6cebc\",\"created_at\":1737747111,\"tags\":[[\"p\",\"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d\"]],\"content\":\"This sample event has the mention example from NIP-19: nostr:nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p\nPls ignore ...\",\"sig\":\"26a4d8f51af0ca7f6c7140ba2487ed4a99edc03e95692e01e636917417dac78989e735559a3a05e1805f242aa634c545fd4d820b962c19301faa55b05bea5da7\"}]").expect("process ok");
}
{
let ndb = Ndb::new(db, &Config::new()).expect("ndb");
let id =
hex::decode("06449981af8f0e00503ea88deb6c2d3e9d65e1e4f032744d9ca4949dce1ce296")
.expect("hex id");
let pubkey =
hex::decode("3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d")
.expect("fiatjaf pubkey");
let txn = Transaction::new(&ndb).expect("txn");
let id_bytes: [u8; 32] = id.try_into().expect("id bytes");
let pubkey_bytes: [u8; 32] = pubkey.try_into().expect("pubkey bytes");
let note = ndb.get_note_by_id(&txn, &id_bytes).unwrap();
let blocks = ndb
.get_blocks_by_key(&txn, note.key().unwrap())
.expect("note");
let mut nprofile_check = false;
for (ndx, block) in blocks.iter(¬e).enumerate() {
println!("block {}: {:?}: {:?}", ndx, block.blocktype(), &block);
if block.blocktype() == BlockType::MentionBech32 {
// https://github.com/nostr-protocol/nips/blob/master/19.md#examples
assert_eq!(
block.as_str(), "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"
);
if let Mention::Profile(p) = block.as_mention().unwrap() {
assert_eq!(p.pubkey(), &pubkey_bytes);
assert_eq!(p.relays.num_relays, 2);
assert_eq!(p.relays.relays[0].as_str(), "wss://r.x.com");
assert_eq!(p.relays.relays[1].as_str(), "wss://djbas.sadkb.com");
let relays: Vec<&str> = p.relays_iter().collect();
assert_eq!(relays, vec!["wss://r.x.com", "wss://djbas.sadkb.com"]);
nprofile_check = true;
}
}
}
assert!(nprofile_check, "Expected nprofile block was not found");
}
test_util::cleanup_db(&db);
}
}