Skip to content

Commit

Permalink
Refactor user relay list, add user muted list processing
Browse files Browse the repository at this point in the history
  • Loading branch information
ksedgwic committed Nov 14, 2024
1 parent 5d352d8 commit e6dc09c
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 71 deletions.
2 changes: 1 addition & 1 deletion enostr/src/pubkey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use nostr::bech32::Hrp;
use std::fmt;
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");
Expand Down
248 changes: 178 additions & 70 deletions src/account_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ use std::collections::{BTreeMap, BTreeSet};
use url::Url;
use uuid::Uuid;

use enostr::{ClientMessage, FilledKeypair, FullKeypair, Keypair, RelayPool};
use enostr::{ClientMessage, FilledKeypair, FullKeypair, Keypair, Pubkey, RelayPool};
use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction};
use serde::{Deserialize, Serialize};

use crate::{
column::Columns,
imgcache::ImageCache,
login_manager::AcquireKeyState,
muted::Muted,
route::{Route, Router},
storage::{KeyStorageResponse, KeyStorageType},
ui::{
Expand All @@ -31,8 +32,156 @@ pub struct AccountRelayData {
advertised: BTreeSet<String>, // advertised via NIP-65
}

impl AccountRelayData {
pub fn new(ndb: &Ndb, pubkey: &[u8; 32]) -> Self {
// Construct a filter for the user's NIP-65 relay list
let filter = Filter::new()
.authors([pubkey])
.kinds([10002])
.limit(1)
.build();

// Local ndb subscription
let ndbsub = ndb
.subscribe(&[filter.clone()])
.expect("ndb relay list subscription");

// Query the ndb immediately to see if the user 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 relays results")
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();
let relays = Self::harvest_nip65_relays(ndb, &txn, &nks);
debug!(
"pubkey {}: initial relays {:#?}",
hex::encode(pubkey),
relays
);

// Id for future remote relay subscriptions
let subid = Uuid::new_v4().to_string();

AccountRelayData {
filter,
subid,
sub: Some(ndbsub),
local: BTreeSet::new(),
advertised: relays.into_iter().collect(),
}
}

// standardize the format (ie, trailing slashes) to avoid dups
pub fn canonicalize_url(url: &str) -> String {
match Url::parse(url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_) => url.to_owned(), // If parsing fails, return the original URL.
}
}

fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec<String> {
nks.iter()
.filter_map(|nk| ndb.get_note_by_key(txn, *nk).ok())
.flat_map(|n| {
n.tags()
.iter()
.filter_map(|ti| ti.get_unchecked(1).variant().str())
.map(|s| s.to_string())
})
.map(|u| Self::canonicalize_url(&u))
.collect()
}
}

pub struct AccountMutedData {
filter: Filter,
subid: String,
sub: Option<Subscription>,
muted: Muted,
}

impl AccountMutedData {
pub fn new(ndb: &Ndb, 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();

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(Pubkey::new(*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(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.
Expand Down Expand Up @@ -128,7 +277,7 @@ impl AccountManager {
let account_data = BTreeMap::new();
let forced_relays: BTreeSet<String> = forced_relays
.into_iter()
.map(|u| Self::canonicalize_url(&u))
.map(|u| AccountRelayData::canonicalize_url(&u))
.collect();
let bootstrap_relays = [
"ws://localhost:8080",
Expand All @@ -140,7 +289,7 @@ impl AccountManager {
]
.iter()
.map(|&url| url.to_string())
.map(|u| Self::canonicalize_url(&u))
.map(|u| AccountRelayData::canonicalize_url(&u))
.collect();

AccountManager {
Expand Down Expand Up @@ -249,6 +398,10 @@ impl AccountManager {
&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,
);
}
}

Expand All @@ -269,67 +422,13 @@ impl AccountManager {
(added, removed)
}

// standardize the format (ie, trailing slashes) to avoid dups
pub fn canonicalize_url(url: &str) -> String {
match Url::parse(url) {
Ok(parsed_url) => parsed_url.to_string(),
Err(_) => url.to_owned(), // If parsing fails, return the original URL.
}
}
fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec<String> {
nks.iter()
.filter_map(|nk| ndb.get_note_by_key(txn, *nk).ok())
.flat_map(|n| {
n.tags()
.iter()
.filter_map(|ti| ti.get_unchecked(1).variant().str())
.map(|s| s.to_string())
})
.map(|u| Self::canonicalize_url(&u))
.collect()
}

fn handle_added_account(&mut self, ndb: &Ndb, pubkey: &[u8; 32]) {
debug!("handle_added_account {}", hex::encode(pubkey));

// Construct a filter for the user's NIP-65 relay list
let filter = Filter::new()
.authors([pubkey])
.kinds([10002])
.limit(1)
.build();

// Local ndb subscription
let ndbsub = ndb.subscribe(&[filter.clone()]).expect("ndb subscription");

// Query the ndb immediately to see if the user 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 relays results")
.iter()
.map(|qr| qr.note_key)
.collect::<Vec<NoteKey>>();
let relays = Self::harvest_nip65_relays(ndb, &txn, &nks);
debug!(
"pubkey {}: initial relays {:#?}",
hex::encode(pubkey),
relays
);

// Id for future remote relay subscriptions
let subid = Uuid::new_v4().to_string();

// Create the user account data
let new_account_data = AccountData {
relay: AccountRelayData {
filter,
subid,
sub: Some(ndbsub),
local: BTreeSet::new(),
advertised: relays.into_iter().collect(),
},
relay: AccountRelayData::new(ndb, pubkey),
muted: AccountMutedData::new(ndb, pubkey),
};
self.account_data.insert(*pubkey, new_account_data);
}
Expand All @@ -345,18 +444,27 @@ impl AccountManager {
for (pubkey, data) in &mut self.account_data {
if let Some(sub) = data.relay.sub {
let nks = ndb.poll_for_notes(sub, 1);
if nks.is_empty() {
continue;
if !nks.is_empty() {
let txn = Transaction::new(ndb).expect("txn");
let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks);
debug!(
"pubkey {}: updated relays {:#?}",
hex::encode(pubkey),
relays
);
data.relay.advertised = relays.into_iter().collect();
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;
}
let txn = Transaction::new(ndb).expect("txn");
let relays = Self::harvest_nip65_relays(ndb, &txn, &nks);
debug!(
"pubkey {}: updated relays {:#?}",
hex::encode(pubkey),
relays
);
data.relay.advertised = relays.into_iter().collect();
changed = true;
}
}
changed
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod imgcache;
mod key_parsing;
pub mod login_manager;
mod multi_subscriber;
mod muted;
mod nav;
mod note;
mod notecache;
Expand Down
23 changes: 23 additions & 0 deletions src/muted.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use enostr::Pubkey;
use std::collections::BTreeSet;

#[derive(Default)]
pub struct Muted {
// TODO - implement private mutes
pub pubkeys: BTreeSet<Pubkey>,
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 {
let threads_hex: Vec<String> = self.threads.iter().map(hex::encode).collect();
f.debug_struct("Muted")
.field("pubkeys", &self.pubkeys)
.field("hashtags", &self.hashtags)
.field("words", &self.words)
.field("threads", &threads_hex)
.finish()
}
}

0 comments on commit e6dc09c

Please sign in to comment.