From ef6d52d1ef007e3cfbad1611005ea30bee94cb0d Mon Sep 17 00:00:00 2001 From: alltheseas Date: Sun, 26 Oct 2025 16:32:43 -0500 Subject: [PATCH] nip17: retain sender copy of private dms --- crates/nostr-sdk/src/client/mod.rs | 104 ++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 10 deletions(-) diff --git a/crates/nostr-sdk/src/client/mod.rs b/crates/nostr-sdk/src/client/mod.rs index 7aac96c5f..28f5b9690 100644 --- a/crates/nostr-sdk/src/client/mod.rs +++ b/crates/nostr-sdk/src/client/mod.rs @@ -39,6 +39,8 @@ pub struct Client { gossip_sync: Arc, } + + impl Default for Client { #[inline] fn default() -> Self { @@ -1006,6 +1008,22 @@ impl Client { Ok(self.pool.send_event_to(urls, event).await?) } + #[cfg(feature = "nip59")] + async fn send_private_wrap( + &self, + event: &Event, + enforce_nip17: bool, + ) -> Result, Error> { + if !self.opts.gossip { + return self.send_event(event).await; + } + + match self.gossip_send_event(event, true).await { + Err(Error::PrivateMsgRelaysNotFound) if !enforce_nip17 => self.send_event(event).await, + res => res, + } + } + /// Build, sign and return [`Event`] /// /// This method requires a [`NostrSigner`]. @@ -1214,15 +1232,16 @@ impl Client { I: IntoIterator, { let signer = self.signer().await?; - let event: Event = - EventBuilder::private_msg(&signer, receiver, message, rumor_extra_tags).await?; + let (receiver_wrap, sender_wrap) = + build_private_dm_wraps(&signer, receiver, message, rumor_extra_tags).await?; - // NOT gossip, send to all relays - if !self.opts.gossip { - return self.send_event(&event).await; - } + // Always enforce NIP-17 routing for the receiver copy. + let output = self.send_private_wrap(&receiver_wrap, true).await?; + + // Send the sender's copy; fall back to default relays if we don't have a NIP-17 list. + let _ = self.send_private_wrap(&sender_wrap, false).await?; - self.gossip_send_event(&event, true).await + Ok(output) } /// Send a private direct message to specific relays @@ -1247,9 +1266,16 @@ impl Client { pool::Error: From<::Err>, { let signer = self.signer().await?; - let event: Event = - EventBuilder::private_msg(&signer, receiver, message, rumor_extra_tags).await?; - self.send_event_to(urls, &event).await + let (receiver_wrap, sender_wrap) = + build_private_dm_wraps(&signer, receiver, message, rumor_extra_tags).await?; + + // The caller provided explicit relay targets for the receiver copy. + let output = self.send_event_to(urls, &receiver_wrap).await?; + + // Still publish the sender copy so the author retains their own history. + let _ = self.send_private_wrap(&sender_wrap, false).await?; + + Ok(output) } /// Construct Gift Wrap and send to relays @@ -1783,3 +1809,61 @@ impl Client { Ok(self.pool.sync_targeted(filters, opts).await?) } } + +#[cfg(feature = "nip59")] +async fn build_private_dm_wraps( + signer: &T, + receiver: PublicKey, + message: S, + rumor_extra_tags: I, +) -> Result<(Event, Event), Error> +where + T: NostrSigner, + S: Into, + I: IntoIterator, +{ + let sender_pubkey: PublicKey = signer.get_public_key().await?; + let rumor: UnsignedEvent = EventBuilder::private_msg_rumor(receiver, message) + .tags(rumor_extra_tags) + .build(sender_pubkey); + + // Clone the rumor so both wraps carry identical payloads/IDs. + let sender_rumor: UnsignedEvent = rumor.clone(); + + let receiver_wrap: Event = EventBuilder::gift_wrap(signer, &receiver, rumor, []).await?; + let sender_wrap: Event = + EventBuilder::gift_wrap(signer, &sender_pubkey, sender_rumor, []).await?; + + Ok((receiver_wrap, sender_wrap)) +} + +#[cfg(all(test, feature = "nip59"))] +mod tests { + use super::*; + + #[tokio::test] + async fn build_private_dm_wraps_produces_sender_copy() { + let sender = Keys::generate(); + let receiver = Keys::generate(); + + let (receiver_wrap, sender_wrap) = build_private_dm_wraps( + &sender, + receiver.public_key(), + "hi there", + [], + ) + .await + .unwrap(); + + assert_eq!(receiver_wrap.kind, Kind::GiftWrap); + assert_eq!(sender_wrap.kind, Kind::GiftWrap); + + let receiver_targets: Vec = + receiver_wrap.tags.public_keys().copied().collect(); + assert_eq!(receiver_targets, vec![receiver.public_key()]); + + let sender_targets: Vec = + sender_wrap.tags.public_keys().copied().collect(); + assert_eq!(sender_targets, vec![sender.public_key()]); + } +}