From b05267c8d54bddf3c2feb485c53204711d881aec Mon Sep 17 00:00:00 2001 From: Bytemalte Date: Fri, 16 Jan 2026 21:24:08 +0100 Subject: [PATCH] feat: improve NIP-17 DM loading logic for incoming and outgoing messages --- src/bin/test_message_loading.rs | 66 ++++++++++++++++++++++++ src/functions/messages.rs | 15 ++++-- src/lib.rs | 2 +- src/nips/nip17.rs | 89 ++++++++++++++++++++------------- 4 files changed, 130 insertions(+), 42 deletions(-) create mode 100644 src/bin/test_message_loading.rs diff --git a/src/bin/test_message_loading.rs b/src/bin/test_message_loading.rs new file mode 100644 index 0000000..d005e41 --- /dev/null +++ b/src/bin/test_message_loading.rs @@ -0,0 +1,66 @@ +use easy_nostr::EasyNostr; +use std::env; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // 1. YOUR PRIVATE KEY (The one who is logged in) + // You can use the HEX format OR the NSEC string. nostr-sdk handles both. + // Example (This must be YOUR secret key): + let my_private_key = env::var("NOSTR_PRIVATE_KEY").unwrap_or_else(|_| { + // This must be a SECRET key (hex or nsec), not a public key! + "hex_nsec_key".to_string() + }); + + // 2. THE CONTACT'S PUBLIC KEY (The person you are chatting with) + let contact_npub = "npub1..."; + + println!("🚀 Initializing EasyNostr with my private key..."); + // EasyNostr::new calls Keys::parse() internally, which accepts Hex or Nsec + let easy_nostr = EasyNostr::new(&my_private_key).await?; + + println!("🌐 Connecting to relays..."); + let relays = vec![ + "wss://relay.damus.io", + "wss://nos.lol", + "wss://relay.snort.social", + "wss://relay.primal.net", // Added Primal as it's very reliable for DMs + ]; + easy_nostr.add_relays(relays).await?; + + println!("📥 Fetching messages for contact: {}...", contact_npub); + + // 3. Fetch the messages + // This will look for: + // - Messages from contact_npub to ME (decrypted with my_private_key) + // - Messages from ME to contact_npub + let messages = easy_nostr.get_private_messages(contact_npub).await?; + + println!("\n--- 💬 Chat History ({}) ---", messages.len()); + + if messages.is_empty() { + println!("❌ No messages found."); + println!( + "Hint: Make sure the private key corresponds to the account that sent/received the messages." + ); + } else { + for msg in messages { + let direction = if msg.is_incoming { + "⬅️ [IN]" + } else { + "➡️ [OUT]" + }; + let time = msg.created_at.to_human_datetime(); + + println!( + "{} | {} | Sender: {:.8}...\n Content: {}\n", + direction, + time, + msg.sender.to_hex(), + msg.content + ); + } + } + + println!("--- End of History ---"); + Ok(()) +} diff --git a/src/functions/messages.rs b/src/functions/messages.rs index 922d2b9..59775b4 100644 --- a/src/functions/messages.rs +++ b/src/functions/messages.rs @@ -1,14 +1,19 @@ +use crate::nips::nip17::{self, Message}; use anyhow::Result; use nostr_sdk::prelude::*; -// Importiere das Message Struct aus nip17! -use crate::nips::nip17::{self, Message}; -pub async fn send_private_message(client: &Client, receiver_pubkey: &str, message: &str) -> Result { +/// Sends a private message. +/// The SDK will automatically handle the NIP-17 (modern) or NIP-04 (legacy) logic. +pub async fn send_private_message( + client: &Client, + receiver_pubkey: &str, + message: &str, +) -> Result { let receiver = PublicKey::parse(receiver_pubkey)?; nip17::send_dm(client, receiver, message).await } -// Rückgabetyp ist Vec +/// Fetches all private messages between the user and a specific contact. pub async fn get_private_messages(client: &Client, contact_npub: &str) -> Result> { nip17::get_dm_messages(client, contact_npub).await -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index f8ce545..f2f6e88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,11 +30,11 @@ impl EasyNostr { messages::send_private_message(&self.client, receiver_pubkey, message).await } + /// Retrieves history of DMs with a specific contact pub async fn get_private_messages(&self, contact_npub: &str) -> Result> { messages::get_private_messages(&self.client, contact_npub).await } - /// Fügt Relays hinzu und verbindet pub async fn add_relays(&self, urls: Vec<&str>) -> Result<()> { relays::add_relays(&self.client, urls).await } diff --git a/src/nips/nip17.rs b/src/nips/nip17.rs index 6ce2101..6c77bb0 100644 --- a/src/nips/nip17.rs +++ b/src/nips/nip17.rs @@ -1,8 +1,8 @@ use anyhow::Result; use nostr_sdk::prelude::*; +use std::collections::HashSet; use std::time::Duration; -/// UI Message Struktur #[derive(Debug, Clone)] pub struct Message { pub id: EventId, @@ -12,79 +12,98 @@ pub struct Message { pub is_incoming: bool, } -/// Direktnachricht senden pub async fn send_dm(client: &Client, receiver: PublicKey, message: &str) -> Result { let output = client.send_private_msg(receiver, message, None).await?; Ok(*output.id()) } -/// Direktnachrichten abrufen +// src/nips/nip17.rs + pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result> { let signer = client.signer().await?; let my_pubkey = signer.get_public_key().await?; let contact_pubkey = PublicKey::parse(contact_npub)?; - // --- Einzelne Filter erstellen --- - let filter_in = Filter::new() + let search_start = Timestamp::now() - Duration::from_secs(60 * 60 * 24 * 365); + + // STRATEGY: We only fetch events where WE are the recipient (#p tag). + // In NIP-17, sent messages are only readable if we sent a copy to ourselves. + // In NIP-04, we can fetch messages we authored. + + let filter_to_me = Filter::new() .kinds([Kind::GiftWrap, Kind::EncryptedDirectMessage]) - .pubkey(my_pubkey) - .since(Timestamp::now() - Duration::from_secs(60 * 60 * 24 * 30)) - .limit(500); + .pubkey(my_pubkey) // All GiftWraps sent TO me (includes my own copies) + .since(search_start); - let filter_out = Filter::new() + let filter_from_me_legacy = Filter::new() .kind(Kind::EncryptedDirectMessage) - .author(my_pubkey) + .author(my_pubkey) // Only for NIP-04 legacy (where author can decrypt) .pubkey(contact_pubkey) - .limit(100); + .since(search_start); - // --- Abrufen: zwei einzelne fetches, dann Ergebnisse mergen --- let mut events = client - .fetch_events(filter_in, Duration::from_secs(10)) - .await?; + .fetch_events(filter_to_me, Duration::from_secs(10)) + .await? + .to_vec(); let more_events = client - .fetch_events(filter_out, Duration::from_secs(10)) - .await?; - events.extend(more_events.into_iter()); + .fetch_events(filter_from_me_legacy, Duration::from_secs(10)) + .await? + .to_vec(); + events.extend(more_events); + let mut seen_ids = HashSet::new(); let mut messages: Vec = Vec::new(); - for event in events.iter() { - // === NIP-17 (Gift Wrap) Verarbeitung === + for event in events { + if !seen_ids.insert(event.id) { + continue; + } + + // === NIP-17 Processing === if event.kind == Kind::GiftWrap { - if let Ok(unwrapped) = client.unwrap_gift_wrap(event).await { + if let Ok(unwrapped) = client.unwrap_gift_wrap(&event).await { let rumor = unwrapped.rumor; - // Allow TextNote (1) or ChatMessage (14) - if rumor.pubkey == contact_pubkey - && (rumor.kind == Kind::TextNote || rumor.kind == Kind::from(14)) + + // 1. INCOMING: Rumor author is contact + let is_incoming = rumor.pubkey == contact_pubkey; + + // 2. OUTGOING: Rumor is from ME, and it contains a tag pointing to the contact + // Most NIP-17 rumors have a 'p' tag inside the rumor to show who it's for. + let is_outgoing = rumor.pubkey == my_pubkey + && rumor.tags.iter().any(|t| { + if let Some(TagStandard::PublicKey { public_key, .. }) = t.as_standardized() + { + public_key == &contact_pubkey + } else { + false + } + }); + + if (is_incoming || is_outgoing) + && (rumor.kind == Kind::from(14) || rumor.kind == Kind::TextNote) { messages.push(Message { id: event.id, sender: rumor.pubkey, content: rumor.content.clone(), created_at: rumor.created_at, - is_incoming: true, + is_incoming, }); } } } - // === NIP-04 (Legacy) Verarbeitung === + // === NIP-04 Processing === else if event.kind == Kind::EncryptedDirectMessage { - // TagStandard::PublicKey ist ein Struct-Variant: Felder mit Struct-Syntax matchen - let has_contact_tag = event.tags.iter().any(|t| match t.as_standardized() { - Some(TagStandard::PublicKey { public_key, .. }) => *public_key == contact_pubkey, - _ => false, - }); - let is_incoming = event.pubkey == contact_pubkey; - let is_outgoing = event.pubkey == my_pubkey && has_contact_tag; + let is_outgoing = event.pubkey == my_pubkey; // Already filtered by p-tag in the filter if is_incoming || is_outgoing { - let counterparty = if is_incoming { + let decrypt_with = if is_incoming { event.pubkey } else { contact_pubkey }; - if let Ok(content) = signer.nip04_decrypt(&counterparty, &event.content).await { + if let Ok(content) = signer.nip04_decrypt(&decrypt_with, &event.content).await { messages.push(Message { id: event.id, sender: event.pubkey, @@ -97,8 +116,6 @@ pub async fn get_dm_messages(client: &Client, contact_npub: &str) -> Result