Skip to content

Commit

Permalink
sdk: add support to NIP17 relay list
Browse files Browse the repository at this point in the history
When `gossip` is enabled, automatically discovery NIP17 for sending and or receiving private direct messages

Closes #640

Signed-off-by: Yuki Kishimoto <[email protected]>
  • Loading branch information
yukibtc committed Nov 24, 2024
1 parent 10601c8 commit 35a08f9
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 113 deletions.
5 changes: 4 additions & 1 deletion bindings/nostr-sdk-ffi/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,10 @@ impl Client {
.into())
}

/// Send private direct message to all relays
/// Send a private direct message
///
/// If gossip is enabled, the message will be sent to the NIP17 relays (automatically discovered).
/// If gossip is not enabled will be sent to all relays with WRITE` relay service flag.
///
/// <https://github.com/nostr-protocol/nips/blob/master/17.md>
#[uniffi::method(default(rumor_extra_tags = []))]
Expand Down
5 changes: 4 additions & 1 deletion bindings/nostr-sdk-js/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,10 @@ impl JsClient {
.map(|id| id.into())
}

/// Send private direct message to all relays
/// Send a private direct message
///
/// If gossip is enabled, the message will be sent to the NIP17 relays (automatically discovered).
/// If gossip is not enabled will be sent to all relays with WRITE` relay service flag.
///
/// <https://github.com/nostr-protocol/nips/blob/master/17.md>
#[wasm_bindgen(js_name = sendPrivateMsg)]
Expand Down
16 changes: 5 additions & 11 deletions crates/nostr-sdk/examples/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ async fn main() -> Result<()> {
tracing_subscriber::fmt::init();

let keys = Keys::parse("nsec12kcgs78l06p30jz7z7h3n2x2cy99nw2z6zspjdp7qc206887mwvs95lnkx")?;
let client = Client::new(keys.clone());
let client = Client::builder()
.signer(keys.clone())
.opts(Options::new().gossip(true))
.build();

println!("Bot public key: {}", keys.public_key().to_bech32()?);

Expand Down Expand Up @@ -50,17 +53,8 @@ async fn main() -> Result<()> {
),
};

// Build private message
let event =
EventBuilder::private_msg(&keys, sender, content, []).await?;

// Send private message
// client.send_event(event).await?;

// Send private message to specific relays
client
.send_event_to(["wss://auth.nostr1.com"], event)
.await?;
client.send_private_msg(sender, content, []).await?;
}
}
Err(e) => tracing::error!("Impossible to decrypt direct message: {e}"),
Expand Down
6 changes: 3 additions & 3 deletions crates/nostr-sdk/examples/nostr-connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ async fn main() -> Result<()> {
let output = client.send_event_builder(builder).await?;
println!("Published text note: {}\n", output.id());

let signer = client.signer().await?;
let receiver =
PublicKey::from_bech32("npub1drvpzev3syqt0kjrls50050uzf25gehpz9vgdw08hvex7e0vgfeq0eseet")?;
let event = EventBuilder::private_msg(&signer, receiver, "Hello from rust-nostr", []).await?;
let output = client.send_event(event).await?;
let output = client
.send_private_msg(receiver, "Hello from rust-nostr", [])
.await?;
println!("Sent DM: {}", output.id());

Ok(())
Expand Down
149 changes: 93 additions & 56 deletions crates/nostr-sdk/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ pub enum Error {
/// Broken down filters for gossip are empty
#[error("gossip broken down filters are empty")]
GossipFiltersEmpty,
/// DMs relays not found
#[error("DMs relays not found")]
DMsRelaysNotFound,
/// Metadata not found
#[error("metadata not found")]
MetadataNotFound,
Expand Down Expand Up @@ -1014,57 +1017,7 @@ impl Client {
return Ok(self.pool.send_event(event).await?);
}

// ########## Gossip ##########

// Get all public keys involved in the event
let public_keys = event
.tags
.public_keys()
.copied()
.chain(iter::once(event.pubkey));

// Check what are up-to-date in the gossip graph and which ones require an update
let outdated_public_keys = self.gossip_graph.check_outdated(public_keys).await;
self.update_outdated_gossip_graph(outdated_public_keys)
.await?;

// Get relays
let mut outbox = self.gossip_graph.get_outbox_relays(&[event.pubkey]).await;
let inbox = self
.gossip_graph
.get_inbox_relays(event.tags.public_keys())
.await;

// Add outbox relays
for url in outbox.iter() {
if self.add_gossip_relay(url).await? {
self.connect_relay(url).await?;
}
}

// Add inbox relays
for url in inbox.iter() {
if self.add_gossip_relay(url).await? {
self.connect_relay(url).await?;
}
}

// Get WRITE relays
// TODO: avoid clone of both url and relay
let write_relays = self
.pool
.relays_with_flag(RelayServiceFlags::WRITE, FlagCheck::All)
.await
.into_keys();

// Extend OUTBOX relays with WRITE ones
outbox.extend(write_relays);

// Union of OUTBOX (and WRITE) with INBOX relays
let urls = outbox.union(&inbox);

// Send event
Ok(self.pool.send_event_to(urls, event).await?)
self.gossip_send_event(event, false).await
}

/// Send multiple events at once to all relays with [`RelayServiceFlags::WRITE`] flag.
Expand Down Expand Up @@ -1325,7 +1278,10 @@ impl Client {
Ok(contacts)
}

/// Send private direct message to all relays
/// Send a private direct message
///
/// If `gossip` is enabled (see [`Options::gossip`]) the message will be sent to the NIP17 relays (automatically discovered).
/// If gossip is not enabled will be sent to all relays with [`RelayServiceFlags::WRITE`] flag.
///
/// <https://github.com/nostr-protocol/nips/blob/master/17.md>
#[inline]
Expand All @@ -1343,10 +1299,16 @@ impl Client {
let signer = self.signer().await?;
let event: Event =
EventBuilder::private_msg(&signer, receiver, message, rumor_extra_tags).await?;
self.send_event(event).await

// NOT gossip, send to all relays
if !self.opts.gossip {
return self.send_event(event).await;
}

self.gossip_send_event(event, true).await
}

/// Send private direct message to specific relays
/// Send a private direct message to specific relays
///
/// <https://github.com/nostr-protocol/nips/blob/master/17.md>
#[inline]
Expand Down Expand Up @@ -1693,8 +1655,8 @@ impl Client {
if !outdated_public_keys.is_empty() {
// Compose filters
let filter: Filter = Filter::default()
.authors(outdated_public_keys)
.kind(Kind::RelayList);
.authors(outdated_public_keys.clone())
.kinds([Kind::RelayList, Kind::InboxRelays]);

// Query from database
let database = self.database();
Expand All @@ -1716,6 +1678,11 @@ impl Client {
.fetch_events_from(relays, vec![filter], Some(Duration::from_secs(10)))
.await?;

// Update last check for these public keys
self.gossip_graph
.update_last_check(outdated_public_keys)
.await;

// Merge database and relays events
let merged: Events = events.merge(stored_events);

Expand Down Expand Up @@ -1779,6 +1746,76 @@ impl Client {
Ok(broken_down.filters)
}

async fn gossip_send_event(&self, event: Event, nip17: bool) -> Result<Output<EventId>, Error> {
// Get all public keys involved in the event
let public_keys = event
.tags
.public_keys()
.copied()
.chain(iter::once(event.pubkey));

// Check what are up to date in the gossip graph and which ones require an update
let outdated_public_keys = self.gossip_graph.check_outdated(public_keys).await;
self.update_outdated_gossip_graph(outdated_public_keys)
.await?;

let urls: HashSet<Url> = if nip17 && event.kind == Kind::GiftWrap {
// Get NIP17 relays
// Get only for relays for p tags since gift wraps are signed with random key (random author)
let relays = self
.gossip_graph
.get_nip17_inbox_relays(event.tags.public_keys())
.await;

if relays.is_empty() {
return Err(Error::DMsRelaysNotFound);
}

// Add outbox and inbox relays
for url in relays.iter() {
if self.add_gossip_relay(url).await? {
self.connect_relay(url).await?;
}
}

relays
} else {
// Get NIP65 relays
let mut outbox = self
.gossip_graph
.get_nip65_outbox_relays(&[event.pubkey])
.await;
let inbox = self
.gossip_graph
.get_nip65_inbox_relays(event.tags.public_keys())
.await;

// Add outbox and inbox relays
for url in outbox.iter().chain(inbox.iter()) {
if self.add_gossip_relay(url).await? {
self.connect_relay(url).await?;
}
}

// Get WRITE relays
// TODO: avoid clone of both url and relay
let write_relays = self
.pool
.relays_with_flag(RelayServiceFlags::WRITE, FlagCheck::All)
.await
.into_keys();

// Extend OUTBOX relays with WRITE ones
outbox.extend(write_relays);

// Union of OUTBOX (and WRITE) with INBOX relays
outbox.union(&inbox).cloned().collect()
};

// Send event
Ok(self.pool.send_event_to(urls, event).await?)
}

async fn gossip_stream_events(
&self,
filters: Vec<Filter>,
Expand Down
1 change: 1 addition & 0 deletions crates/nostr-sdk/src/gossip/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ use std::time::Duration;
/// Max number of relays allowed in NIP17/NIP65 lists
pub const MAX_RELAYS_LIST: usize = 5;
pub const PUBKEY_METADATA_OUTDATED_AFTER: Duration = Duration::from_secs(60 * 60); // 60 min
pub const CHECK_OUTDATED_INTERVAL: Duration = Duration::from_secs(60 * 5); // 5 min
Loading

0 comments on commit 35a08f9

Please sign in to comment.