notedeck

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

commit d5956319660823d86d875f17144a4258c7f4b690
parent f00a67ab2cd83049af4d4862e12b5609af301459
Author: Ken Sedgwick <ken@bonsai.com>
Date:   Fri, 15 Nov 2024 08:36:56 -0800

Add user mute list sync via polling

Diffstat:
Menostr/src/pubkey.rs | 2+-
Msrc/accounts/mod.rs | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 1+
Asrc/muted.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 167 insertions(+), 1 deletion(-)

diff --git a/enostr/src/pubkey.rs b/enostr/src/pubkey.rs @@ -6,7 +6,7 @@ use std::fmt; use std::ops::Deref; use tracing::debug; -#[derive(Eq, PartialEq, Clone, Copy, Hash)] +#[derive(Eq, PartialEq, Clone, Copy, Hash, Ord, PartialOrd)] pub struct Pubkey([u8; 32]); static HRP_NPUB: Hrp = Hrp::parse_unchecked("npub"); diff --git a/src/accounts/mod.rs b/src/accounts/mod.rs @@ -11,6 +11,7 @@ use crate::{ column::Columns, imgcache::ImageCache, login_manager::AcquireKeyState, + muted::Muted, route::{Route, Router}, storage::{KeyStorageResponse, KeyStorageType}, ui::{ @@ -116,8 +117,98 @@ impl AccountRelayData { } } +pub struct AccountMutedData { + filter: Filter, + subid: String, + sub: Option<Subscription>, + muted: Muted, +} + +impl AccountMutedData { + pub fn new(ndb: &Ndb, pool: &mut RelayPool, pubkey: &[u8; 32]) -> Self { + // Construct a filter for the user's NIP-51 muted list + let filter = Filter::new() + .authors([pubkey]) + .kinds([10000]) + .limit(1) + .build(); + + // Local ndb subscription + let ndbsub = ndb + .subscribe(&[filter.clone()]) + .expect("ndb muted subscription"); + + // Query the ndb immediately to see if the user's muted list is already there + let txn = Transaction::new(ndb).expect("transaction"); + let lim = filter.limit().unwrap_or(crate::filter::default_limit()) as i32; + let nks = ndb + .query(&txn, &[filter.clone()], lim) + .expect("query user muted results") + .iter() + .map(|qr| qr.note_key) + .collect::<Vec<NoteKey>>(); + let muted = Self::harvest_nip51_muted(ndb, &txn, &nks); + debug!("pubkey {}: initial muted {:?}", hex::encode(pubkey), muted); + + // Id for future remote relay subscriptions + let subid = Uuid::new_v4().to_string(); + + // Add remote subscription to existing relays + pool.subscribe(subid.clone(), vec![filter.clone()]); + + AccountMutedData { + filter, + subid, + sub: Some(ndbsub), + muted, + } + } + + fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted { + let mut muted = Muted::default(); + for nk in nks.iter() { + if let Ok(note) = ndb.get_note_by_key(txn, *nk) { + for tag in note.tags() { + match tag.get(0).and_then(|t| t.variant().str()) { + Some("p") => { + if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { + muted.pubkeys.insert(*id); + } + } + Some("t") => { + if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { + muted.hashtags.insert(str.to_string()); + } + } + Some("word") => { + if let Some(str) = tag.get(1).and_then(|f| f.variant().str()) { + muted.words.insert(str.to_string()); + } + } + Some("e") => { + if let Some(id) = tag.get(1).and_then(|f| f.variant().id()) { + muted.threads.insert(*id); + } + } + Some("alt") => { + // maybe we can ignore these? + } + Some(x) => error!("query_nip51_muted: unexpected tag: {}", x), + None => error!( + "query_nip51_muted: bad tag value: {:?}", + tag.get_unchecked(0).variant() + ), + } + } + } + } + muted + } +} + pub struct AccountData { relay: AccountRelayData, + muted: AccountMutedData, } /// The interface for managing the user's accounts. @@ -355,6 +446,10 @@ impl Accounts { &ClientMessage::req(data.relay.subid.clone(), vec![data.relay.filter.clone()]), relay_url, ); + pool.send_to( + &ClientMessage::req(data.muted.subid.clone(), vec![data.muted.filter.clone()]), + relay_url, + ); } } @@ -381,6 +476,7 @@ impl Accounts { // Create the user account data let new_account_data = AccountData { relay: AccountRelayData::new(ndb, pool, pubkey), + muted: AccountMutedData::new(ndb, pool, pubkey), }; self.account_data.insert(*pubkey, new_account_data); } @@ -408,6 +504,16 @@ impl Accounts { changed = true; } } + if let Some(sub) = data.muted.sub { + let nks = ndb.poll_for_notes(sub, 1); + if !nks.is_empty() { + let txn = Transaction::new(ndb).expect("txn"); + let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); + debug!("pubkey {}: updated muted {:?}", hex::encode(pubkey), muted); + data.muted.muted = muted; + changed = true; + } + } } changed } diff --git a/src/lib.rs b/src/lib.rs @@ -21,6 +21,7 @@ mod imgcache; mod key_parsing; pub mod login_manager; mod multi_subscriber; +mod muted; mod nav; mod note; mod notecache; diff --git a/src/muted.rs b/src/muted.rs @@ -0,0 +1,59 @@ +use nostrdb::Note; +use std::collections::BTreeSet; + +use tracing::debug; + +#[derive(Default)] +pub struct Muted { + // TODO - implement private mutes + pub pubkeys: BTreeSet<[u8; 32]>, + pub hashtags: BTreeSet<String>, + pub words: BTreeSet<String>, + pub threads: BTreeSet<[u8; 32]>, +} + +impl std::fmt::Debug for Muted { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Muted") + .field( + "pubkeys", + &self.pubkeys.iter().map(hex::encode).collect::<Vec<_>>(), + ) + .field("hashtags", &self.hashtags) + .field("words", &self.words) + .field( + "threads", + &self.threads.iter().map(hex::encode).collect::<Vec<_>>(), + ) + .finish() + } +} + +impl Muted { + pub fn is_muted(&self, note: &Note) -> bool { + if self.pubkeys.contains(note.pubkey()) { + debug!( + "{}: MUTED pubkey: {}", + hex::encode(note.id()), + hex::encode(note.pubkey()) + ); + return true; + } + // FIXME - Implement hashtag muting here + + // TODO - let's not add this for now, we will likely need to + // have an optimized data structure in nostrdb to properly + // mute words. this mutes substrings which is not ideal. + // + // let content = note.content().to_lowercase(); + // for word in &self.words { + // if content.contains(&word.to_lowercase()) { + // debug!("{}: MUTED word: {}", hex::encode(note.id()), word); + // return true; + // } + // } + + // FIXME - Implement thread muting here + false + } +}