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:
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)
+ }
+}