notedeck

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

commit 3278d3ba165bcc6d0c4c0bd4a61de5b06b026da3
parent fe3e2dad14dd5d14eeb2e2e950ce51c13d9202c1
Author: Ken Sedgwick <ken@bonsai.com>
Date:   Wed, 22 Jan 2025 12:43:05 -0800

upgrade url string to  RelaySpec for [read|write] markers

I think RelaySpec wants to move to enostr so the RelayPool can support
read and write relays ...

Diffstat:
Mcrates/notedeck/src/accounts.rs | 54++++++++++++++++++++++++++++++++++++------------------
Mcrates/notedeck/src/lib.rs | 2++
Acrates/notedeck/src/relayspec.rs | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 128 insertions(+), 18 deletions(-)

diff --git a/crates/notedeck/src/accounts.rs b/crates/notedeck/src/accounts.rs @@ -1,7 +1,8 @@ use tracing::{debug, error, info}; use crate::{ - KeyStorageResponse, KeyStorageType, MuteFun, Muted, SingleUnkIdAction, UnknownIds, UserAccount, + KeyStorageResponse, KeyStorageType, MuteFun, Muted, RelaySpec, SingleUnkIdAction, UnknownIds, + UserAccount, }; use enostr::{ClientMessage, FilledKeypair, Keypair, RelayPool}; use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; @@ -38,8 +39,8 @@ pub struct AccountRelayData { filter: Filter, subid: String, sub: Option<Subscription>, - local: BTreeSet<String>, // used locally but not advertised - advertised: BTreeSet<String>, // advertised via NIP-65 + local: BTreeSet<RelaySpec>, // used locally but not advertised + advertised: BTreeSet<RelaySpec>, // advertised via NIP-65 } #[derive(Default)] @@ -107,7 +108,7 @@ impl AccountRelayData { } } - fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec<String> { + fn harvest_nip65_relays(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Vec<RelaySpec> { let mut relays = Vec::new(); for nk in nks.iter() { if let Ok(note) = ndb.get_note_by_key(txn, *nk) { @@ -115,7 +116,17 @@ impl AccountRelayData { match tag.get(0).and_then(|t| t.variant().str()) { Some("r") => { if let Some(url) = tag.get(1).and_then(|f| f.variant().str()) { - relays.push(Self::canonicalize_url(url)); + let has_read_marker = tag + .get(2) + .map_or(false, |m| m.variant().str() == Some("read")); + let has_write_marker = tag + .get(2) + .map_or(false, |m| m.variant().str() == Some("write")); + relays.push(RelaySpec::new( + Self::canonicalize_url(url), + has_read_marker, + has_write_marker, + )); } } Some("alt") => { @@ -236,8 +247,8 @@ pub struct Accounts { accounts: Vec<UserAccount>, key_store: KeyStorageType, account_data: BTreeMap<[u8; 32], AccountData>, - forced_relays: BTreeSet<String>, - bootstrap_relays: BTreeSet<String>, + forced_relays: BTreeSet<RelaySpec>, + bootstrap_relays: BTreeSet<RelaySpec>, needs_relay_config: bool, } @@ -251,9 +262,9 @@ impl Accounts { let currently_selected_account = get_selected_index(&accounts, &key_store); let account_data = BTreeMap::new(); - let forced_relays: BTreeSet<String> = forced_relays + let forced_relays: BTreeSet<RelaySpec> = forced_relays .into_iter() - .map(|u| AccountRelayData::canonicalize_url(&u)) + .map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false)) .collect(); let bootstrap_relays = [ "wss://relay.damus.io", @@ -264,7 +275,7 @@ impl Accounts { ] .iter() .map(|&url| url.to_string()) - .map(|u| AccountRelayData::canonicalize_url(&u)) + .map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false)) .collect(); Accounts { @@ -526,20 +537,26 @@ impl Accounts { debug!("current relays: {:?}", pool.urls()); debug!("desired relays: {:?}", desired_relays); - let add: BTreeSet<String> = desired_relays.difference(&pool.urls()).cloned().collect(); - let mut sub: BTreeSet<String> = pool.urls().difference(&desired_relays).cloned().collect(); + let pool_specs = pool + .urls() + .iter() + .map(|url| RelaySpec::new(url.clone(), false, false)) + .collect(); + let add: BTreeSet<RelaySpec> = desired_relays.difference(&pool_specs).cloned().collect(); + let mut sub: BTreeSet<RelaySpec> = + pool_specs.difference(&desired_relays).cloned().collect(); if !add.is_empty() { debug!("configuring added relays: {:?}", add); - let _ = pool.add_urls(add, wakeup); + let _ = pool.add_urls(add.iter().map(|r| r.url.clone()).collect(), wakeup); } if !sub.is_empty() { - debug!("removing unwanted relays: {:?}", sub); - // certain relays are persistent like the multicast relay, // although we should probably have a way to explicitly // disable it - sub.remove("multicast"); - pool.remove_urls(&sub); + sub.remove(&RelaySpec::new("multicast", false, false)); + + debug!("removing unwanted relays: {:?}", sub); + pool.remove_urls(&sub.iter().map(|r| r.url.clone()).collect()); } debug!("current relays: {:?}", pool.urls()); @@ -591,6 +608,7 @@ impl Accounts { } pub fn add_advertised_relay(&mut self, relay_to_add: &str) { + let relay_to_add = AccountRelayData::canonicalize_url(relay_to_add); info!("add advertised relay \"{}\"", relay_to_add); match self.currently_selected_account { None => error!("no account is currently selected."), @@ -607,7 +625,7 @@ impl Accounts { // iniitialize with the bootstrapping set. advertised.extend(self.bootstrap_relays.iter().cloned()); } - advertised.insert(relay_to_add.to_string()); + advertised.insert(RelaySpec::new(relay_to_add, false, false)); self.needs_relay_config = true; // FIXME - need to publish the advertised set } diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -10,6 +10,7 @@ mod muted; pub mod note; mod notecache; mod persist; +pub mod relayspec; mod result; pub mod storage; mod style; @@ -33,6 +34,7 @@ pub use muted::{MuteFun, Muted}; pub use note::{NoteRef, RootIdError, RootNoteId, RootNoteIdBuf}; pub use notecache::{CachedNote, NoteCache}; pub use persist::*; +pub use relayspec::RelaySpec; pub use result::Result; pub use storage::{ DataPath, DataPathType, Directory, FileKeyStorage, KeyStorageResponse, KeyStorageType, diff --git a/crates/notedeck/src/relayspec.rs b/crates/notedeck/src/relayspec.rs @@ -0,0 +1,90 @@ +use std::cmp::Ordering; +use std::fmt; + +// A Relay specification includes NIP-65 defined "markers" which +// indicate if the relay should be used for reading or writing (or +// both). + +#[derive(Clone)] +pub struct RelaySpec { + pub url: String, + pub has_read_marker: bool, + pub has_write_marker: bool, +} + +impl RelaySpec { + pub fn new( + url: impl Into<String>, + mut has_read_marker: bool, + mut has_write_marker: bool, + ) -> Self { + // if both markers are set turn both off ... + if has_read_marker && has_write_marker { + has_read_marker = false; + has_write_marker = false; + } + RelaySpec { + url: url.into(), + has_read_marker, + has_write_marker, + } + } + + // The "marker" fields are a little counter-intuitive ... from NIP-65: + // + // "The event MUST include a list of r tags with relay URIs and a read + // or write marker. Relays marked as read / write are called READ / + // WRITE relays, respectively. If the marker is omitted, the relay is + // used for both purposes." + // + pub fn is_readable(&self) -> bool { + !self.has_write_marker // only "write" relays are not readable + } + pub fn is_writable(&self) -> bool { + !self.has_read_marker // only "read" relays are not writable + } +} + +// just the url part +impl fmt::Display for RelaySpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.url) + } +} + +// add the read and write markers if present +impl fmt::Debug for RelaySpec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\"{}\"", self)?; + if self.has_read_marker { + write!(f, " [r]")?; + } + if self.has_write_marker { + write!(f, " [w]")?; + } + Ok(()) + } +} + +// For purposes of set arithmetic only the url is considered, two +// RelaySpec which differ only in markers are the same ... + +impl PartialEq for RelaySpec { + fn eq(&self, other: &Self) -> bool { + self.url == other.url + } +} + +impl Eq for RelaySpec {} + +impl PartialOrd for RelaySpec { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.url.cmp(&other.url)) + } +} + +impl Ord for RelaySpec { + fn cmp(&self, other: &Self) -> Ordering { + self.url.cmp(&other.url) + } +}