From 122299f5489fb473f2e194b2cd34bd9d45012830 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sat, 15 Nov 2025 08:30:45 +0700 Subject: [PATCH] chore: improve nip4e implementation (#204) * patch * update ui * add load response * fix * . * wip: rewrite gossip * new gossip implementation * clean up * . * debug * . * . * update * . * fix * fix --- Cargo.lock | 61 ++--- crates/account/src/lib.rs | 66 ++--- crates/auto_update/src/lib.rs | 2 +- crates/chat/src/lib.rs | 180 ++++++-------- crates/chat/src/room.rs | 143 +++++------ crates/chat_ui/src/lib.rs | 2 +- crates/coop/src/views/compose.rs | 2 +- crates/coop/src/views/edit_profile.rs | 15 +- crates/coop/src/views/new_account.rs | 26 +- crates/coop/src/views/screening.rs | 3 +- crates/coop/src/views/setup_relay.rs | 9 +- crates/coop/src/views/sidebar/mod.rs | 4 +- crates/encryption/src/lib.rs | 238 ++++++++++++------ crates/encryption_ui/src/lib.rs | 343 +++++++++++++++----------- crates/person/src/lib.rs | 5 +- crates/state/src/lib.rs | 185 +++++++------- crates/state/src/storage.rs | 135 ++++++++-- crates/state/src/tracker.rs | 7 - 18 files changed, 847 insertions(+), 579 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfca91d5..6e312044 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,7 +1205,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1628,7 +1628,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "proc-macro2", "quote", @@ -1989,9 +1989,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -2568,7 +2568,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2665,7 +2665,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2676,7 +2676,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "gpui", @@ -2905,7 +2905,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "async-compression", @@ -2920,6 +2920,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "serde_urlencoded", "sha2", "tempfile", "url", @@ -2930,7 +2931,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "rustls", "rustls-platform-verifier", @@ -2944,9 +2945,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f" dependencies = [ "atomic-waker", "bytes", @@ -3735,7 +3736,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -3985,7 +3986,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "aes", "base64", @@ -4009,7 +4010,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "async-utility", "nostr", @@ -4021,7 +4022,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "flatbuffers", "lru", @@ -4032,7 +4033,7 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "nostr", ] @@ -4040,7 +4041,7 @@ dependencies = [ [[package]] name = "nostr-gossip-memory" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "indexmap", "lru", @@ -4052,7 +4053,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "async-utility", "flume", @@ -4066,7 +4067,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "async-utility", "async-wsocket", @@ -4083,7 +4084,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" +source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050" dependencies = [ "async-utility", "nostr", @@ -4594,7 +4595,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "collections", "serde", @@ -5220,7 +5221,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "derive_refineable", ] @@ -5318,7 +5319,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "bytes", @@ -5372,7 +5373,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "arrayvec", "log", @@ -5838,7 +5839,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "serde", @@ -6286,7 +6287,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "arrayvec", "log", @@ -7273,7 +7274,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "anyhow", "async-fs", @@ -7309,7 +7310,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" +source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed" dependencies = [ "perf", "quote", @@ -7721,9 +7722,9 @@ dependencies = [ [[package]] name = "weezl" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs index b4c4730c..92a088b5 100644 --- a/crates/account/src/lib.rs +++ b/crates/account/src/lib.rs @@ -1,4 +1,3 @@ -use std::sync::Arc; use std::time::Duration; use anyhow::Error; @@ -21,10 +20,10 @@ pub struct Account { public_key: Option, /// Status of the current user NIP-65 relays - pub nip65_status: RelayStatus, + pub nip65_status: Entity, /// Status of the current user NIP-17 relays - pub nip17_status: RelayStatus, + pub nip17_status: Entity, /// Tasks for asynchronous operations _tasks: SmallVec<[Task<()>; 2]>, @@ -64,32 +63,31 @@ impl Account { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let nip65_status = cx.new(|_| RelayStatus::default()); + let nip17_status = cx.new(|_| RelayStatus::default()); + let mut tasks = smallvec![]; tasks.push( // Observe the nostr signer and set the public key when it sets - cx.spawn({ - let client = Arc::clone(&client); + cx.spawn(async move |this, cx| { + let result = cx + .background_spawn(async move { Self::observe_signer(&client).await }) + .await; - async move |this, cx| { - let result = cx - .background_spawn(async move { Self::observe_signer(&client).await }) - .await; - - if let Some(public_key) = result { - this.update(cx, |this, cx| { - this.set_account(public_key, cx); - }) - .expect("Entity has been released") - } + if let Some(public_key) = result { + this.update(cx, |this, cx| { + this.set_account(public_key, cx); + }) + .expect("Entity has been released") } }), ); Self { public_key: None, - nip65_status: RelayStatus::default(), - nip17_status: RelayStatus::default(), + nip65_status, + nip17_status, _tasks: tasks, } } @@ -125,8 +123,6 @@ impl Account { .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) .await?; - log::info!("Getting user's gossip relays..."); - Ok(()) } @@ -168,23 +164,29 @@ impl Account { self._tasks.push( // Verify user's nip65 and nip17 relays cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_secs(10)) - .await; + cx.background_executor().timer(Duration::from_secs(5)).await; + // Fetch the NIP-65 relays event in the local database let ensure_nip65 = Self::ensure_nip65_relays(&client, public_key).await; + + // Fetch the NIP-17 relays event in the local database let ensure_nip17 = Self::ensure_nip17_relays(&client, public_key).await; this.update(cx, |this, cx| { - this.nip65_status = match ensure_nip65 { - Ok(true) => RelayStatus::Set, - _ => RelayStatus::NotSet, - }; - this.nip17_status = match ensure_nip17 { - Ok(true) => RelayStatus::Set, - _ => RelayStatus::NotSet, - }; - cx.notify(); + this.nip65_status.update(cx, |this, cx| { + *this = match ensure_nip65 { + Ok(true) => RelayStatus::Set, + _ => RelayStatus::NotSet, + }; + cx.notify(); + }); + this.nip17_status.update(cx, |this, cx| { + *this = match ensure_nip17 { + Ok(true) => RelayStatus::Set, + _ => RelayStatus::NotSet, + }; + cx.notify(); + }); }) .expect("Entity has been released") }), diff --git a/crates/auto_update/src/lib.rs b/crates/auto_update/src/lib.rs index 6208e72c..a6bc9862 100644 --- a/crates/auto_update/src/lib.rs +++ b/crates/auto_update/src/lib.rs @@ -297,7 +297,7 @@ impl AutoUpdater { Err(anyhow!("No update available")) } } else { - Err(anyhow!("Not found")) + Err(anyhow!("No update available")) } }) } diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index c0be7df6..268c2795 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -18,8 +18,7 @@ use nostr_sdk::prelude::*; pub use room::*; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; -use smol::lock::RwLock; -use state::{initialized_at, EventTracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION}; +use state::{initialized_at, NostrRegistry, GIFTWRAP_SUBSCRIPTION}; mod message; mod room; @@ -41,6 +40,9 @@ pub struct ChatRegistry { /// Loading status of the registry pub loading: bool, + /// Async task for handling notifications + handle_notifications: Task<()>, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 1]>, @@ -82,11 +84,19 @@ impl ChatRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let tracker = nostr.read(cx).tracker(); let status = Arc::new(AtomicBool::new(true)); let (tx, rx) = flume::bounded::(2048); + let handle_notifications = cx.background_spawn({ + let client = nostr.read(cx).client(); + let status = Arc::clone(&status); + let tx = tx.clone(); + let signer: Option> = None; + + async move { Self::handle_notifications(&client, &signer, &tx, &status).await } + }); + let mut subscriptions = smallvec![]; let mut tasks = smallvec![]; @@ -98,29 +108,27 @@ impl ChatRegistry { move |this, state, cx| { if let Some(signer) = state.read(cx).clone() { - this.retry_failed_events(&signer, &tx, &status, cx); + this.handle_notifications = cx.background_spawn({ + let client = nostr.read(cx).client(); + let status = Arc::clone(&status); + let tx = tx.clone(); + let signer = Some(signer); + + async move { + Self::handle_notifications(&client, &signer, &tx, &status).await + } + }); + cx.notify(); } } }), ); - tasks.push( - // Handle notifications - cx.background_spawn({ - let client = Arc::clone(&client); - let status = Arc::clone(&status); - let tx = tx.clone(); - - async move { Self::handle_notifications(&client, &tracker, &tx, &status).await } - }), - ); - tasks.push( // Handle unwrapping status - cx.background_spawn({ - let client = Arc::clone(&client); - async move { Self::handle_unwrapping(&client, &status, &tx).await } - }), + cx.background_spawn( + async move { Self::handle_unwrapping(&client, &status, &tx).await }, + ), ); tasks.push( @@ -155,23 +163,24 @@ impl ChatRegistry { Self { rooms: vec![], loading: true, + handle_notifications, _subscriptions: subscriptions, _tasks: tasks, } } - async fn handle_notifications( + async fn handle_notifications( client: &Client, - tracker: &Arc>, + signer: &Option, tx: &Sender, status: &Arc, - ) { - let mut notifications = client.notifications(); - log::info!("Listening for notifications"); - + ) where + T: NostrSigner, + { let initialized_at = initialized_at(); let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); + let mut notifications = client.notifications(); let mut public_keys = HashSet::new(); let mut processed_events = HashSet::new(); @@ -194,7 +203,7 @@ impl ChatRegistry { } // Extract the rumor from the gift wrap event - match Self::extract_rumor(client, event.as_ref()).await { + match Self::extract_rumor(client, signer, event.as_ref()).await { Ok(rumor) => { // Get all public keys public_keys.extend(rumor.all_pubkeys()); @@ -209,7 +218,7 @@ impl ChatRegistry { Self::get_metadata(client, public_keys).await.ok(); } - match &event.created_at >= initialized_at { + match &rumor.created_at >= initialized_at { true => { let new_message = NewMessage::new(event.id, rumor); let signal = Signal::Message(new_message); @@ -223,11 +232,8 @@ impl ChatRegistry { } } } - Err(_e) => { - let mut tracker = tracker.write().await; - tracker.failed_unwrap_events.push(event.as_ref().clone()); - - drop(tracker); + Err(e) => { + log::warn!("Failed to unwrap gift wrap event: {}", e); } } } @@ -280,46 +286,6 @@ impl ChatRegistry { } } - fn retry_failed_events( - &mut self, - signer: &Arc, - tx: &Sender, - status: &Arc, - cx: &mut Context, - ) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let tracker = nostr.read(cx).tracker(); - - let signer = Arc::clone(signer); - let status = Arc::clone(status); - - let tx = tx.clone(); - let initialized_at = initialized_at(); - - self._tasks.push(cx.background_spawn(async move { - let tracker = tracker.read().await; - - for event in tracker.failed_unwrap_events.iter() { - if let Ok(rumor) = Self::try_unwrap_custom(&client, &signer, event).await { - match &event.created_at >= initialized_at { - true => { - let new_message = NewMessage::new(event.id, rumor); - let signal = Signal::Message(new_message); - - if let Err(e) = tx.send_async(signal).await { - log::error!("Failed to send signal: {}", e); - } - } - false => { - status.store(true, Ordering::Release); - } - } - } - } - })); - } - /// Set the loading status of the chat registry pub fn set_loading(&mut self, loading: bool, cx: &mut Context) { self.loading = loading; @@ -600,14 +566,21 @@ impl ChatRegistry { } // Unwraps a gift-wrapped event and processes its contents. - async fn extract_rumor(client: &Client, gift_wrap: &Event) -> Result { + async fn extract_rumor( + client: &Client, + signer: &Option, + gift_wrap: &Event, + ) -> Result + where + T: NostrSigner, + { // Try to get cached rumor first if let Ok(event) = Self::get_rumor(client, gift_wrap.id).await { return Ok(event); } // Try to unwrap with the available signer - let unwrapped = Self::try_unwrap(client, gift_wrap).await?; + let unwrapped = Self::try_unwrap(client, signer, gift_wrap).await?; let mut rumor_unsigned = unwrapped.rumor; // Generate event id for the rumor if it doesn't have one @@ -620,34 +593,45 @@ impl ChatRegistry { } // Helper method to try unwrapping with different signers - async fn try_unwrap(client: &Client, gift_wrap: &Event) -> Result { + async fn try_unwrap( + client: &Client, + signer: &Option, + gift_wrap: &Event, + ) -> Result + where + T: NostrSigner, + { + if let Some(custom_signer) = signer.as_ref() { + if let Ok(seal) = custom_signer + .nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content) + .await + { + let seal: Event = Event::from_json(seal)?; + seal.verify_with_ctx(SECP256K1)?; + + // Decrypt the rumor + // TODO: verify the sender + let rumor = custom_signer + .nip44_decrypt(&seal.pubkey, &seal.content) + .await?; + + // Construct the unsigned event + let rumor = UnsignedEvent::from_json(rumor)?; + + // Return the unwrapped gift + return Ok(UnwrappedGift { + sender: rumor.pubkey, + rumor, + }); + } + } + let signer = client.signer().await?; let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?; Ok(unwrapped) } - /// Helper method to try unwrapping with a custom signer - async fn try_unwrap_custom( - client: &Client, - signer: &T, - gift_wrap: &Event, - ) -> Result - where - T: NostrSigner, - { - let unwrapped = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await?; - let mut rumor_unsigned = unwrapped.rumor; - - // Generate event id for the rumor if it doesn't have one - rumor_unsigned.ensure_id(); - - // Cache the rumor - Self::set_rumor(client, gift_wrap.id, &rumor_unsigned).await?; - - Ok(rumor_unsigned) - } - /// Stores an unwrapped event in local database with reference to original async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> { let rumor_id = rumor.id.context("Rumor is missing an event id")?; @@ -718,7 +702,7 @@ impl ChatRegistry { { let authors: Vec = public_keys.into_iter().collect(); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; + let kinds = vec![Kind::Metadata, Kind::ContactList]; // Return if the list is empty if authors.is_empty() { @@ -726,7 +710,7 @@ impl ChatRegistry { } let filter = Filter::new() - .limit(authors.len() * kinds.len() + 10) + .limit(authors.len() * kinds.len()) .authors(authors) .kinds(kinds); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index ecb5a0b7..0ad78789 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -8,6 +8,7 @@ use anyhow::{anyhow, Error}; use common::{EventUtils, RenderedProfile}; use encryption::{Encryption, SignerKind}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; +use itertools::Itertools; use nostr_sdk::prelude::*; use person::PersonRegistry; use state::NostrRegistry; @@ -326,7 +327,7 @@ impl Room { cx.emit(RoomSignal::Refresh); } - /// Get messaging relays and encryption keys announcement for each member + /// Get gossip relays for each member pub fn connect(&self, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -342,22 +343,10 @@ impl Room { continue; }; - // Construct a filter for messaging relays - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(member) - .limit(1); + // Construct a filter for gossip relays + let filter = Filter::new().kind(Kind::RelayList).author(member).limit(1); - // Subscribe to get members messaging relays - client.subscribe(filter, Some(opts)).await?; - - // Construct a filter for encryption keys announcement - let filter = Filter::new() - .kind(Kind::Custom(10044)) - .author(member) - .limit(1); - - // Subscribe to get members encryption keys announcement + // Subscribe to get member's gossip relays client.subscribe(filter, Some(opts)).await?; } @@ -376,15 +365,15 @@ impl Room { .kind(Kind::ApplicationSpecificData) .custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id); - let stored = client.database().query(filter).await?; - - let mut messages: Vec = stored + let messages = client + .database() + .query(filter) + .await? .into_iter() .filter_map(|event| UnsignedEvent::from_json(&event.content).ok()) + .sorted_by_key(|message| message.created_at) .collect(); - messages.sort_by_key(|message| message.created_at); - Ok(messages) }) } @@ -392,8 +381,8 @@ impl Room { /// Create a new message event (unsigned) pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { let nostr = NostrRegistry::global(cx); - let cache = nostr.read(cx).cache_manager(); - let cache = cache.read_blocking(); + let gossip = nostr.read(cx).gossip(); + let read_gossip = gossip.read_blocking(); // Get current user let account = Account::global(cx); @@ -409,9 +398,7 @@ impl Room { // NOTE: current user will be removed from the list of receivers for member in self.members.iter() { // Get relay hint if available - let relay_url = cache - .relay(member) - .and_then(|urls| urls.iter().nth(0).cloned()); + let relay_url = read_gossip.messaging_relays(member).first().cloned(); // Construct a public key tag with relay hint let tag = TagStandard::PublicKey { @@ -470,7 +457,7 @@ impl Room { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let cache = nostr.read(cx).cache_manager(); + let gossip = nostr.read(cx).gossip(); let tracker = nostr.read(cx).tracker(); let rumor = rumor.to_owned(); @@ -481,8 +468,11 @@ impl Room { cx.background_spawn(async move { let signer_kind = opts.signer_kind; - let cache = cache.read().await; - let tracker = tracker.read().await; + let gossip = gossip.read().await; + + // Get current user's signer and public key + let user_signer = client.signer().await?; + let user_pubkey = user_signer.get_public_key().await?; // Get the encryption public key let encryption_pubkey = if let Some(signer) = encryption_key.as_ref() { @@ -491,9 +481,6 @@ impl Room { None }; - let user_signer = client.signer().await?; - let user_pubkey = user_signer.get_public_key().await?; - // Remove the current user's public key from the list of receivers // the current user will be handled separately members.retain(|&pk| pk != user_pubkey); @@ -506,7 +493,9 @@ impl Room { for member in members.into_iter() { // Get user's messaging relays - let urls = cache.relay(&member).cloned().unwrap_or_default(); + let urls = gossip.messaging_relays(&member); + // Get user's encryption public key if available + let encryption = gossip.announcement(&member).map(|a| a.public_key()); // Check if there are any relays to send the message to if urls.is_empty() { @@ -514,35 +503,26 @@ impl Room { continue; } - // Ensure connection to all messaging relays - for url in urls.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } - - // Get user's encryption public key if available - let encryption = cache - .announcement(&member) - .and_then(|a| a.to_owned().map(|a| a.public_key())); - // Skip sending if using encryption signer but receiver's encryption keys not found if encryption.is_none() && matches!(signer_kind, SignerKind::Encryption) { reports.push(SendReport::new(member).device_not_found()); continue; } - let receiver = Self::select_receiver(&signer_kind, member, encryption)?; - let rumor = rumor.clone(); + // Ensure connections to the relays + gossip.ensure_connections(&client, &urls).await; - // Construct the sealed event - let seal = EventBuilder::seal(&signer, &receiver, rumor.clone()) - .await? - .build(member) - .sign(&signer) - .await?; + // Determine the receiver based on the signer kind + let receiver = Self::select_receiver(&signer_kind, member, encryption)?; // Construct the gift wrap event - let event = EventBuilder::gift_wrap_from_seal(&member, &seal, vec![])?; + let event = EventBuilder::gift_wrap( + &signer, + &receiver, + rumor.clone(), + vec![Tag::public_key(member)], + ) + .await?; // Send the gift wrap event to the messaging relays match client.send_event_to(urls, &event).await { @@ -554,6 +534,7 @@ impl Room { if auth { // Wait for authenticated and resent event successfully for attempt in 0..=SEND_RETRY { + let tracker = tracker.read().await; let ids = tracker.resent_ids(); // Check if event was successfully resent @@ -581,22 +562,34 @@ impl Room { } } - let receiver = Self::select_receiver(&signer_kind, user_pubkey, encryption_pubkey)?; - let rumor = rumor.clone(); + // Return early if the user disabled backup. + // + // Coop will not send a gift wrap event to the current user. + if !opts.backup() { + return Ok(reports); + } - // Construct the sealed event - let seal = EventBuilder::seal(&signer, &receiver, rumor.clone()) - .await? - .build(user_pubkey) - .sign(&signer) - .await?; + // Skip sending if using encryption signer but receiver's encryption keys not found + if encryption_pubkey.is_none() && matches!(signer_kind, SignerKind::Encryption) { + reports.push(SendReport::new(user_pubkey).device_not_found()); + return Ok(reports); + } + + // Determine the receiver based on the signer kind + let receiver = Self::select_receiver(&signer_kind, user_pubkey, encryption_pubkey)?; // Construct the gift-wrapped event - let event = EventBuilder::gift_wrap_from_seal(&receiver, &seal, vec![])?; + let event = EventBuilder::gift_wrap( + &signer, + &receiver, + rumor.clone(), + vec![Tag::public_key(user_pubkey)], + ) + .await?; // Only send a backup message to current user if sent successfully to others - if opts.backup() && reports.iter().all(|r| r.is_sent_success()) { - let urls = cache.relay(&user_pubkey).cloned().unwrap_or_default(); + if reports.iter().all(|r| r.is_sent_success()) { + let urls = gossip.messaging_relays(&user_pubkey); // Check if there are any relays to send the event to if urls.is_empty() { @@ -604,11 +597,8 @@ impl Room { return Ok(reports); } - // Ensure connection to all messaging relays - for url in urls.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } + // Ensure connections to the relays + gossip.ensure_connections(&client, &urls).await; // Send the event to the messaging relays match client.send_event_to(urls, &event).await { @@ -635,10 +625,10 @@ impl Room { ) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let cache_manager = nostr.read(cx).cache_manager(); + let gossip = nostr.read(cx).gossip(); cx.background_spawn(async move { - let cache = cache_manager.read().await; + let gossip = gossip.read().await; let mut resend_reports = vec![]; for report in reports.into_iter() { @@ -667,12 +657,15 @@ impl Room { // Process the on hold event if it exists if let Some(event) = report.on_hold { - let urls = cache.relay(&receiver).cloned().unwrap_or_default(); + let urls = gossip.messaging_relays(&receiver); // Check if there are any relays to send the event to if urls.is_empty() { resend_reports.push(SendReport::new(receiver).relays_not_found()); } else { + // Ensure connections to the relays + gossip.ensure_connections(&client, &urls).await; + // Send the event to the messaging relays match client.send_event_to(urls, &event).await { Ok(output) => { @@ -705,15 +698,15 @@ impl Room { fn select_receiver( kind: &SignerKind, - members: PublicKey, + member: PublicKey, encryption: Option, ) -> Result { match kind { SignerKind::Encryption => { Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?) } - SignerKind::User => Ok(members), - SignerKind::Auto => Ok(encryption.unwrap_or(members)), + SignerKind::User => Ok(member), + SignerKind::Auto => Ok(encryption.unwrap_or(member)), } } } diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index e1486a59..ec040cad 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -272,13 +272,13 @@ impl ChatPanel { // Get the current room entity let room = self.room.read(cx); + let opts = self.options.read(cx); // Create a temporary message for optimistic update let rumor = room.create_message(&content, replies.as_ref(), cx); let rumor_id = rumor.id.unwrap(); // Create a task for sending the message in the background - let opts = self.options.read(cx); let send_message = room.send_message(&rumor, opts, cx); // Optimistically update message list diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index e1d41cfb..912cdc57 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -197,7 +197,7 @@ impl Compose { async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> { let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; + let kinds = vec![Kind::Metadata, Kind::ContactList]; let filter = Filter::new().author(public_key).kinds(kinds).limit(10); client diff --git a/crates/coop/src/views/edit_profile.rs b/crates/coop/src/views/edit_profile.rs index d6fcfa3f..186b54b9 100644 --- a/crates/coop/src/views/edit_profile.rs +++ b/crates/coop/src/views/edit_profile.rs @@ -175,9 +175,6 @@ impl EditProfile { } pub fn set_metadata(&mut self, cx: &mut Context) -> Task> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let avatar = self.avatar_input.read(cx).value().to_string(); let name = self.name_input.read(cx).value().to_string(); let bio = self.bio_input.read(cx).value().to_string(); @@ -199,14 +196,24 @@ impl EditProfile { new_metadata = new_metadata.website(url); } + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); + cx.background_spawn(async move { let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let gossip = gossip.read().await; + let write_relays = gossip.inbox_relays(&public_key); + + // Ensure connections to the write relays + gossip.ensure_connections(&client, &write_relays).await; // Sign the new metadata event let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?; // Send event to user's write relayss - client.send_event(&event).await?; + client.send_event_to(write_relays, &event).await?; // Return the updated profile let metadata = Metadata::from_json(&event.content).unwrap_or_default(); diff --git a/crates/coop/src/views/new_account.rs b/crates/coop/src/views/new_account.rs index 816c0351..424007a5 100644 --- a/crates/coop/src/views/new_account.rs +++ b/crates/coop/src/views/new_account.rs @@ -137,11 +137,12 @@ impl NewAccount { // Verify the signer let signer = client.signer().await?; + let nip65_relays = default_nip65_relays(); // Construct a NIP-65 event let event = EventBuilder::new(Kind::RelayList, "") .tags( - default_nip65_relays() + nip65_relays .iter() .cloned() .map(|(url, metadata)| Tag::relay_metadata(url, metadata)), @@ -152,6 +153,25 @@ impl NewAccount { // Set NIP-65 relays client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; + // Ensure relays are connected + for (url, _metadata) in nip65_relays.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + // Extract only write relays + let write_relays: Vec = nip65_relays + .iter() + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == &Some(RelayMetadata::Write) { + Some(url.to_owned()) + } else { + None + } + }) + .take(3) + .collect(); + // Construct a NIP-17 event let event = EventBuilder::new(Kind::InboxRelays, "") .tags(default_nip17_relays().iter().cloned().map(Tag::relay)) @@ -159,13 +179,13 @@ impl NewAccount { .await?; // Set NIP-17 relays - client.send_event(&event).await?; + client.send_event_to(&write_relays, &event).await?; // Construct a metadata event let event = EventBuilder::metadata(&metadata).sign(&signer).await?; // Set metadata - client.send_event(&event).await?; + client.send_event_to(&write_relays, &event).await?; Ok(()) }); diff --git a/crates/coop/src/views/screening.rs b/crates/coop/src/views/screening.rs index b1233b7b..1e4a0877 100644 --- a/crates/coop/src/views/screening.rs +++ b/crates/coop/src/views/screening.rs @@ -1,4 +1,3 @@ -use std::sync::Arc; use std::time::Duration; use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS}; @@ -44,7 +43,7 @@ impl Screening { let mut tasks = smallvec![]; let contact_check: Task), Error>> = cx.background_spawn({ - let client = Arc::clone(&client); + let client = nostr.read(cx).client(); async move { let signer = client.signer().await?; let signer_pubkey = signer.get_public_key().await?; diff --git a/crates/coop/src/views/setup_relay.rs b/crates/coop/src/views/setup_relay.rs index c28aad09..3ee33291 100644 --- a/crates/coop/src/views/setup_relay.rs +++ b/crates/coop/src/views/setup_relay.rs @@ -155,10 +155,17 @@ impl SetupRelay { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); let relays = self.relays.clone(); let task: Task> = cx.background_spawn(async move { let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let gossip = gossip.read().await; + let write_relays = gossip.inbox_relays(&public_key); + + // Ensure connections to the write relays + gossip.ensure_connections(&client, &write_relays).await; let tags: Vec = relays .iter() @@ -171,7 +178,7 @@ impl SetupRelay { .await?; // Set messaging relays - client.send_event(&event).await?; + client.send_event_to(write_relays, &event).await?; // Connect to messaging relays for relay in relays.iter() { diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 2ee69dac..e0585bca 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -137,15 +137,13 @@ impl Sidebar { async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> { let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; + let kinds = vec![Kind::Metadata, Kind::ContactList]; let filter = Filter::new().author(public_key).kinds(kinds).limit(10); client .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) .await?; - log::info!("Subscribe to get metadata for: {public_key}"); - Ok(()) } diff --git a/crates/encryption/src/lib.rs b/crates/encryption/src/lib.rs index bf231444..c80ff77a 100644 --- a/crates/encryption/src/lib.rs +++ b/crates/encryption/src/lib.rs @@ -49,10 +49,10 @@ pub struct Encryption { handle_requests: Option>, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, + _subscriptions: SmallVec<[Subscription; 2]>, /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 2]>, + _tasks: SmallVec<[Task<()>; 1]>, } impl Encryption { @@ -69,48 +69,36 @@ impl Encryption { /// Create a new encryption instance fn new(cx: &mut Context) -> Self { let account = Account::global(cx); - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); let requests = cx.new(|_| HashSet::default()); let encryption = cx.new(|_| None); let client_signer = cx.new(|_| None); let mut subscriptions = smallvec![]; - let mut tasks = smallvec![]; subscriptions.push( // Observe the account state cx.observe(&account, |this, state, cx| { - if state.read(cx).has_account() && !this.has_encryption(cx) { + if state.read(cx).has_account() && this.client_signer.read(cx).is_none() { + this.get_client(cx); + } + }), + ); + + subscriptions.push( + // Observe the client signer state + cx.observe(&client_signer, |this, state, cx| { + if state.read(cx).is_some() { this.get_announcement(cx); } }), ); - tasks.push( - // Get the client key - cx.spawn(async move |this, cx| { - match Self::get_keys(&client, "client").await { - Ok(keys) => { - this.update(cx, |this, cx| { - this.set_client(Arc::new(keys), cx); - }) - .expect("Entity has been released"); - } - Err(_) => { - let keys = Keys::generate(); - let secret = keys.secret_key().to_secret_hex(); - - // Store the key in the database for future use - Self::set_keys(&client, "client", secret).await.ok(); - - // Update global state - this.update(cx, |this, cx| { - this.set_client(Arc::new(keys), cx); - }) - .expect("Entity has been released"); - } + subscriptions.push( + // Observe the encryption signer state + cx.observe(&encryption, |this, state, cx| { + if state.read(cx).is_some() { + this._tasks.push(this.resubscribe_messages(cx)); } }), ); @@ -123,7 +111,7 @@ impl Encryption { handle_notifications: None, handle_requests: None, _subscriptions: subscriptions, - _tasks: tasks, + _tasks: smallvec![], } } @@ -174,14 +162,50 @@ impl Encryption { } } + /// Get the client keys from the database + fn get_client(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + self._tasks.push( + // Run in the main thread + cx.spawn(async move |this, cx| { + match Self::get_keys(&client, "client").await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_client(Arc::new(keys), cx); + }) + .expect("Entity has been released"); + } + Err(_) => { + let keys = Keys::generate(); + let secret = keys.secret_key().to_secret_hex(); + + // Store the key in the database for future use + Self::set_keys(&client, "client", secret).await.ok(); + + // Update global state + this.update(cx, |this, cx| { + this.set_client(Arc::new(keys), cx); + }) + .expect("Entity has been released"); + } + } + }), + ) + } + + /// Get the announcement from the database fn get_announcement(&mut self, cx: &mut Context) { let task = self._get_announcement(cx); + let delay = Duration::from_secs(10); - self._tasks.push(cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(5)).await; + self._tasks.push( + // Run task in the background + cx.spawn(async move |this, cx| { + cx.background_executor().timer(delay).await; - match task.await { - Ok(announcement) => { + if let Ok(announcement) = task.await { this.update(cx, |this, cx| { this.load_encryption(&announcement, cx); // Set the announcement @@ -190,11 +214,8 @@ impl Encryption { }) .expect("Entity has been released"); } - Err(err) => { - log::error!("Failed to get announcement: {}", err); - } - }; - })); + }), + ); } fn _get_announcement(&self, cx: &App) -> Task> { @@ -236,12 +257,65 @@ impl Encryption { this.listen_request(cx); } } + this.load_response(cx); }) .expect("Entity has been released"); }) .detach(); } + pub fn load_response(&mut self, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + // Get the client signer + let Some(client_signer) = self.client_signer.read(cx).clone() else { + return; + }; + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .author(public_key) + .kind(Kind::Custom(4455)) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + let response = NostrRegistry::extract_response(&client, &event).await?; + + // Decrypt the payload using the client signer + let decrypted = client_signer + .nip44_decrypt(&response.public_key(), response.payload()) + .await?; + + // Construct the encryption keys + let secret = SecretKey::parse(&decrypted)?; + let keys = Keys::new(secret); + + return Ok(keys); + } + + Err(anyhow!("not found")) + }); + + cx.spawn(async move |this, cx| { + match task.await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_encryption(Arc::new(keys), cx); + }) + .expect("Entity has been released"); + } + Err(e) => { + log::warn!("Failed to load encryption response: {e}"); + } + }; + }) + .detach(); + } + /// Listen for the encryption key request from other devices /// /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md @@ -252,7 +326,7 @@ impl Encryption { let (tx, rx) = flume::bounded::(50); let task: Task> = cx.background_spawn({ - let client = Arc::clone(&client); + let client = nostr.read(cx).client(); async move { let signer = client.signer().await?; @@ -325,6 +399,7 @@ impl Encryption { pub fn new_encryption(&self, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); let keys = Keys::generate(); let public_key = keys.public_key(); @@ -336,6 +411,12 @@ impl Encryption { Self::set_keys(&client, "encryption", secret).await?; let signer = client.signer().await?; + let signer_pubkey = signer.get_public_key().await?; + let gossip = gossip.read().await; + let write_relays = gossip.inbox_relays(&signer_pubkey); + + // Ensure connections to the write relays + gossip.ensure_connections(&client, &write_relays).await; // Construct the announcement event let event = EventBuilder::new(Kind::Custom(10044), "") @@ -343,11 +424,12 @@ impl Encryption { Tag::client(app_name()), Tag::custom(TagKind::custom("n"), vec![public_key]), ]) + .build(signer_pubkey) .sign(&signer) .await?; // Send the announcement event to user's relays - client.send_event(&event).await?; + client.send_event_to(write_relays, &event).await?; Ok(keys) }) @@ -359,6 +441,7 @@ impl Encryption { pub fn send_request(&self, cx: &App) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); // Get the client signer let Some(client_signer) = self.client_signer.read(cx).clone() else { @@ -395,6 +478,12 @@ impl Encryption { Ok(Some(keys)) } None => { + let gossip = gossip.read().await; + let write_relays = gossip.inbox_relays(&public_key); + + // Ensure connections to the write relays + gossip.ensure_connections(&client, &write_relays).await; + // Construct encryption keys request event let event = EventBuilder::new(Kind::Custom(4454), "") .tags(vec![ @@ -405,7 +494,7 @@ impl Encryption { .await?; // Send a request for encryption keys from other devices - client.send_event(&event).await?; + client.send_event_to(&write_relays, &event).await?; // Create a unique ID to control the subscription later let subscription_id = SubscriptionId::new("listen-response"); @@ -413,12 +502,11 @@ impl Encryption { let filter = Filter::new() .kind(Kind::Custom(4455)) .author(public_key) - .pubkey(client_pubkey) .since(Timestamp::now()); // Subscribe to the approval response event client - .subscribe_with_id(subscription_id, filter, None) + .subscribe_with_id_to(&write_relays, subscription_id, filter, None) .await?; Ok(None) @@ -433,6 +521,7 @@ impl Encryption { pub fn send_response(&self, target: PublicKey, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); // Get the client signer let Some(client_signer) = self.client_signer.read(cx).clone() else { @@ -440,6 +529,14 @@ impl Encryption { }; cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + let gossip = gossip.read().await; + let write_relays = gossip.inbox_relays(&public_key); + + // Ensure connections to the write relays + gossip.ensure_connections(&client, &write_relays).await; + let encryption = Self::get_keys(&client, "encryption").await?; let client_pubkey = client_signer.get_public_key().await?; @@ -457,30 +554,12 @@ impl Encryption { Tag::custom(TagKind::custom("P"), vec![client_pubkey]), Tag::public_key(target), ]) - .sign(&client_signer) + .build(public_key) + .sign(&signer) .await?; - // Get the current user's signer and public key - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - // Get the current user's relay list - let urls: Vec = client - .database() - .relay_list(public_key) - .await? - .into_iter() - .filter_map(|(url, metadata)| { - if metadata.is_none() || metadata == Some(RelayMetadata::Read) { - Some(url) - } else { - None - } - }) - .collect(); - // Send the response event to the user's relay list - client.send_event_to(urls, &event).await?; + client.send_event_to(write_relays, &event).await?; Ok(()) }) @@ -493,12 +572,14 @@ impl Encryption { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let client_signer = self.client_signer.read(cx).clone().unwrap(); - let mut processed_events = HashSet::new(); + // Get the client signer + let Some(client_signer) = self.client_signer.read(cx).clone() else { + return Task::ready(Err(anyhow!("Client Signer is required"))); + }; cx.background_spawn(async move { let mut notifications = client.notifications(); - log::info!("Listening for notifications"); + let mut processed_events = HashSet::new(); while let Ok(notification) = notifications.recv().await { let RelayPoolNotification::Message { message, .. } = notification else { @@ -513,7 +594,7 @@ impl Encryption { } if event.kind != Kind::Custom(4455) { - // Skip non-gift wrap events + // Skip non-response events continue; } @@ -528,6 +609,8 @@ impl Encryption { let keys = Keys::new(secret); return Ok(keys); + } else { + log::error!("Failed to extract response from event"); } } } @@ -579,4 +662,21 @@ impl Encryption { cx.notify(); }); } + + /// Resubscribe to gift wrap events + fn resubscribe_messages(&self, cx: &App) -> Task<()> { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); + + let account = Account::global(cx); + let public_key = account.read(cx).public_key(); + + cx.background_spawn(async move { + let gossip = gossip.read().await; + let relays = gossip.messaging_relays(&public_key); + + NostrRegistry::get_messages(&client, public_key, &relays).await; + }) + } } diff --git a/crates/encryption_ui/src/lib.rs b/crates/encryption_ui/src/lib.rs index 675363cf..ba4a7eca 100644 --- a/crates/encryption_ui/src/lib.rs +++ b/crates/encryption_ui/src/lib.rs @@ -1,3 +1,5 @@ +use std::cell::Cell; +use std::rc::Rc; use std::sync::Arc; use std::time::Duration; @@ -181,6 +183,7 @@ impl EncryptionPanel { }); } Err(e) => { + this.set_requesting(false, cx); this.set_error(e.to_string(), cx); } }; @@ -193,11 +196,13 @@ impl EncryptionPanel { fn ask_for_approval(&mut self, req: Announcement, window: &mut Window, cx: &mut Context) { let client_name = SharedString::from(req.client().to_string()); let target = req.public_key(); + let id = SharedString::from(req.id().to_hex()); + let loading = Rc::new(Cell::new(false)); let note = Notification::new() - .custom_id(SharedString::from(req.id().to_hex())) + .custom_id(id.clone()) .autohide(false) - .icon(IconName::Info) + .icon(IconName::Encryption) .title(SharedString::from("Encryption Key Request")) .content(move |_window, cx| { v_flex() @@ -208,14 +213,24 @@ impl EncryptionPanel { )) .child( v_flex() - .py_1() - .px_1p5() - .rounded_sm() - .text_xs() + .h_12() + .items_center() + .justify_center() + .px_2() + .rounded(cx.theme().radius) .bg(cx.theme().warning_background) .text_color(cx.theme().warning_foreground) .child(client_name.clone()), ) + .child( + h_flex() + .h_7() + .w_full() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(SharedString::from(target.to_hex())), + ) .into_any_element() }) .action(move |_window, _cx| { @@ -223,13 +238,38 @@ impl EncryptionPanel { .label("Approve") .small() .primary() - .loading(false) - .disabled(false) - .on_click(move |_ev, _window, cx| { - let encryption = Encryption::global(cx); - let send_response = encryption.read(cx).send_response(target, cx); + .loading(loading.get()) + .disabled(loading.get()) + .on_click({ + let loading = Rc::clone(&loading); + let id = id.clone(); - send_response.detach(); + move |_ev, window, cx| { + // Set loading state to true + loading.set(true); + + let encryption = Encryption::global(cx); + let send_response = encryption.read(cx).send_response(target, cx); + let id = id.clone(); + + window + .spawn(cx, async move |cx| { + let result = send_response.await; + + cx.update(|window, cx| { + match result { + Ok(_) => { + window.clear_notification_by_id(id, cx); + } + Err(e) => { + window.push_notification(e.to_string(), cx); + } + }; + }) + .expect("Entity has been released"); + }) + .detach(); + } }) }); @@ -252,6 +292,7 @@ impl Render for EncryptionPanel { const WARNING: &str = "Encryption Key is still in the alpha stage. Please be cautious."; let encryption = Encryption::global(cx); + let announcement = encryption.read(cx).announcement(); let has_encryption = encryption.read(cx).has_encryption(cx); v_flex() @@ -259,149 +300,165 @@ impl Render for EncryptionPanel { .max_w(px(340.)) .w(px(340.)) .text_sm() - .when(has_encryption, |this| { - this.child( - h_flex() - .gap_2() - .w_full() - .text_xs() - .font_semibold() - .child( - Icon::new(IconName::CheckCircleFill) - .small() - .text_color(cx.theme().element_active), - ) - .child(SharedString::from("Encryption Key has been set")), - ) - }) - .when(!has_encryption, |this| { - if let Some(announcement) = encryption.read(cx).announcement().as_ref() { - let pubkey = shorten_pubkey(announcement.public_key(), 16); - let name = announcement.client(); + .when_some(announcement.as_ref(), |this, announcement| { + let pubkey = shorten_pubkey(announcement.public_key(), 16); + let name = announcement.client(); - this.child( - v_flex() - .gap_2() - .child(div().font_semibold().child(SharedString::from(NOTICE))) - .child( - v_flex() - .h_12() - .items_center() - .justify_center() - .rounded(cx.theme().radius) - .bg(cx.theme().warning_background) - .text_color(cx.theme().warning_foreground) - .child(name), + this.child( + v_flex() + .gap_2() + .when(has_encryption, |this| { + this.child( + h_flex() + .gap_1p5() + .text_sm() + .font_semibold() + .child( + Icon::new(IconName::CheckCircle) + .text_color(cx.theme().element_foreground) + .small(), + ) + .child(SharedString::from("Encryption Key has been set")), ) - .child( + }) + .when(!has_encryption, |this| { + this.child(div().font_semibold().child(SharedString::from(NOTICE))) + }) + .child( + v_flex() + .gap_1() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Client Name:")), + ) + .child( + h_flex() + .h_12() + .items_center() + .justify_center() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(name), + ), + ) + .child( + v_flex() + .gap_1() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Client Public Key:")), + ) + .child( + h_flex() + .h_7() + .w_full() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(SharedString::from(pubkey)), + ), + ) + .when(!has_encryption, |this| { + this.child( v_flex() - .gap_1() + .gap_2() .child( div() .text_xs() - .font_semibold() .text_color(cx.theme().text_muted) - .child(SharedString::from("Client Public Key:")), + .child(SharedString::from(SUGGEST)), ) .child( h_flex() - .h_7() - .w_full() - .px_2() - .rounded(cx.theme().radius) - .bg(cx.theme().elevated_surface_background) - .child(SharedString::from(pubkey)), + .mt_2() + .gap_1() + .when(!self.requesting, |this| { + this.child( + Button::new("reset") + .label("Reset") + .flex_1() + .small() + .ghost_alt() + .loading(self.creating) + .disabled(self.creating) + .on_click(cx.listener( + move |this, _ev, window, cx| { + this.new_encryption(window, cx); + }, + )), + ) + }) + .when(!self.creating, |this| { + this.child( + Button::new("request") + .label({ + if self.requesting { + "Wait for approval" + } else { + "Request" + } + }) + .flex_1() + .small() + .primary() + .loading(self.requesting) + .disabled(self.requesting) + .on_click(cx.listener( + move |this, _ev, window, cx| { + this.request(window, cx); + }, + )), + ) + }), ), ) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from(SUGGEST)), - ) - .child( - h_flex() - .mt_2() - .gap_1() - .when(!self.requesting, |this| { - this.child( - Button::new("reset") - .label("Reset") - .flex_1() - .small() - .ghost_alt() - .loading(self.creating) - .disabled(self.creating) - .on_click(cx.listener( - move |this, _ev, window, cx| { - this.new_encryption(window, cx); - }, - )), - ) - }) - .when(!self.creating, |this| { - this.child( - Button::new("request") - .label({ - if self.requesting { - "Wait for approval" - } else { - "Request" - } - }) - .flex_1() - .small() - .primary() - .loading(self.requesting) - .disabled(self.requesting) - .on_click(cx.listener( - move |this, _ev, window, cx| { - this.request(window, cx); - }, - )), - ) - }), - ) - .when_some(self.error.read(cx).as_ref(), |this, error| { - this.child( - div() - .text_xs() - .text_center() - .text_color(cx.theme().danger_foreground) - .child(error.clone()), - ) - }), - ) - } else { - this.child( - v_flex() - .gap_2() - .child( - div() - .font_semibold() - .child(SharedString::from("Set up Encryption Key")), - ) - .child(SharedString::from(DESCRIPTION)) - .child( - div() - .text_xs() - .text_color(cx.theme().warning_foreground) - .child(SharedString::from(WARNING)), - ) - .child( - Button::new("create") - .label("Setup") - .flex_1() - .small() - .primary() - .loading(self.creating) - .disabled(self.creating) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.new_encryption(window, cx); - })), - ), - ) - } + }), + ) + }) + .when_none(&announcement, |this| { + this.child( + v_flex() + .gap_2() + .child( + div() + .font_semibold() + .child(SharedString::from("Set up Encryption Key")), + ) + .child(SharedString::from(DESCRIPTION)) + .child( + div() + .text_xs() + .text_color(cx.theme().warning_foreground) + .child(SharedString::from(WARNING)), + ) + .child( + Button::new("create") + .label("Setup") + .flex_1() + .small() + .primary() + .loading(self.creating) + .disabled(self.creating) + .on_click(cx.listener(move |this, _ev, window, cx| { + this.new_encryption(window, cx); + })), + ), + ) + }) + .when_some(self.error.read(cx).as_ref(), |this, error| { + this.child( + div() + .text_xs() + .text_center() + .text_color(cx.theme().danger_foreground) + .child(error.clone()), + ) }) } } diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 7797f73c..5e24ffcb 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -1,5 +1,4 @@ use std::collections::{HashMap, HashSet}; -use std::sync::Arc; use gpui::{App, AppContext, Context, Entity, Global, Task}; use nostr_sdk::prelude::*; @@ -44,12 +43,10 @@ impl PersonRegistry { tasks.push( // Handle notifications cx.spawn({ - let client = Arc::clone(&client); + let client = nostr.read(cx).client(); async move |this, cx| { let mut notifications = client.notifications(); - log::info!("Listening for notifications"); - let mut processed_events = HashSet::new(); while let Ok(notification) = notifications.recv().await { diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index d34c5ed2..522a93fd 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -16,8 +16,7 @@ pub use tracker::*; mod storage; mod tracker; -pub const GIFTWRAP_SUBSCRIPTION: &str = "default-inbox"; -pub const ENCRYPTION_GIFTWARP_SUBSCRIPTION: &str = "encryption-inbox"; +pub const GIFTWRAP_SUBSCRIPTION: &str = "gift-wrap-events"; pub fn init(cx: &mut App) { NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); @@ -30,15 +29,15 @@ impl Global for GlobalNostrRegistry {} /// Nostr Registry #[derive(Debug)] pub struct NostrRegistry { - /// Nostr client instance - client: Arc, + /// Nostr Client + client: Client, + + /// Custom gossip implementation + gossip: Arc>, /// Tracks activity related to Nostr events tracker: Arc>, - /// Manages caching of nostr events - cache_manager: Arc>, - /// Tasks for asynchronous operations _tasks: SmallVec<[Task<()>; 1]>, } @@ -63,11 +62,7 @@ impl NostrRegistry { .install_default() .ok(); - let path = config_dir().join("nostr"); - let lmdb = NostrLMDB::open(path).expect("Failed to initialize database"); - let gossip = NostrGossipMemory::unbounded(); - - // Nostr client options + // Construct the nostr client options let opts = ClientOptions::new() .automatic_authentication(false) .verify_subscriptions(false) @@ -76,16 +71,12 @@ impl NostrRegistry { }); // Construct the nostr client - let client = Arc::new( - ClientBuilder::default() - .gossip(gossip) - .database(lmdb) - .opts(opts) - .build(), - ); + let path = config_dir().join("nostr"); + let lmdb = NostrLMDB::open(path).expect("Failed to initialize database"); + let client = ClientBuilder::default().database(lmdb).opts(opts).build(); let tracker = Arc::new(RwLock::new(EventTracker::default())); - let cache_manager = Arc::new(RwLock::new(CacheManager::default())); + let gossip = Arc::new(RwLock::new(Gossip::default())); let mut tasks = smallvec![]; @@ -94,8 +85,8 @@ impl NostrRegistry { // // And handle notifications from the nostr relay pool channel cx.background_spawn({ - let client = Arc::clone(&client); - let cache_manager = Arc::clone(&cache_manager); + let client = client.clone(); + let gossip = Arc::clone(&gossip); let tracker = Arc::clone(&tracker); let _ = initialized_at(); @@ -104,7 +95,7 @@ impl NostrRegistry { Self::connect(&client).await; // Handle notifications from the relay pool - Self::handle_notifications(&client, &cache_manager, &tracker).await; + Self::handle_notifications(&client, &gossip, &tracker).await; } }), ); @@ -112,7 +103,7 @@ impl NostrRegistry { Self { client, tracker, - cache_manager, + gossip, _tasks: tasks, } } @@ -135,12 +126,10 @@ impl NostrRegistry { async fn handle_notifications( client: &Client, - cache: &Arc>, + gossip: &Arc>, tracker: &Arc>, ) { let mut notifications = client.notifications(); - log::info!("Listening for notifications"); - let mut processed_events = HashSet::new(); while let Ok(notification) = notifications.recv().await { @@ -158,53 +147,50 @@ impl NostrRegistry { match event.kind { Kind::RelayList => { - if Self::is_self_authored(client, &event).await { - log::info!("Found relay list event for the current user"); - let author = event.pubkey; - let announcement = Kind::Custom(10044); + let mut gossip = gossip.write().await; + gossip.insert_relays(&event); - // Fetch user's messaging relays event - _ = Self::subscribe(client, author, Kind::InboxRelays).await; - // Fetch user's encryption announcement event - _ = Self::subscribe(client, author, announcement).await; + let urls: Vec = Self::extract_write_relays(&event); + let author = event.pubkey; + + // Fetch user's encryption announcement event + Self::get(client, &urls, author, Kind::Custom(10044)).await; + // Fetch user's messaging relays event + Self::get(client, &urls, author, Kind::InboxRelays).await; + + // Verify if the event is belonging to the current user + if Self::is_self_authored(client, &event).await { // Fetch user's metadata event - _ = Self::subscribe(client, author, Kind::Metadata).await; + Self::get(client, &urls, author, Kind::Metadata).await; // Fetch user's contact list event - _ = Self::subscribe(client, author, Kind::ContactList).await; + Self::get(client, &urls, author, Kind::ContactList).await; } } Kind::InboxRelays => { - // Extract up to 3 messaging relays - let urls: Vec = - nip17::extract_relay_list(&event).take(3).cloned().collect(); + let mut gossip = gossip.write().await; + gossip.insert_messaging_relays(&event); - // Subscribe to gift wrap events if event is from current user if Self::is_self_authored(client, &event).await { - log::info!("Found messaging list event for the current user"); + // Extract user's messaging relays + let urls: Vec = + nip17::extract_relay_list(&event).cloned().collect(); - if let Err(e) = - Self::get_messages(client, &urls, event.pubkey).await - { - log::error!("Failed to subscribe to gift wrap events: {e}"); - } + // Fetch user's inbox messages in the extracted relays + Self::get_messages(client, event.pubkey, &urls).await; } - - // Cache the messaging relays - let mut cache = cache.write().await; - cache.insert_relay(event.pubkey, urls); } Kind::Custom(10044) => { - // Cache the announcement event - if let Ok(announcement) = Self::extract_announcement(&event) { - let mut cache = cache.write().await; - cache.insert_announcement(event.pubkey, Some(announcement)); - } + let mut gossip = gossip.write().await; + gossip.insert_announcement(&event); } Kind::ContactList => { if Self::is_self_authored(client, &event).await { - let pubkeys: Vec<_> = event.tags.public_keys().copied().collect(); + let public_keys: Vec = + event.tags.public_keys().copied().collect(); - if let Err(e) = Self::get_metadata_for_list(client, pubkeys).await { + if let Err(e) = + Self::get_metadata_for_list(client, public_keys).await + { log::error!("Failed to get metadata for list: {e}"); } } @@ -242,47 +228,59 @@ impl NostrRegistry { false } - /// Subscribe for events that match the given kind for a given author - async fn subscribe(client: &Client, author: PublicKey, kind: Kind) -> Result<(), Error> { + /// Get event that match the given kind for a given author + async fn get(client: &Client, urls: &[RelayUrl], author: PublicKey, kind: Kind) { + // Skip if no relays are provided + if urls.is_empty() { + return; + } + + // Ensure relay connections + for url in urls.iter() { + client.add_relay(url).await.ok(); + client.connect_relay(url).await.ok(); + } + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let filter = Filter::new().author(author).kind(kind).limit(1); // Subscribe to filters from the user's write relays - client.subscribe(filter, Some(opts)).await?; - - Ok(()) + if let Err(e) = client.subscribe_to(urls, filter, Some(opts)).await { + log::error!("Failed to subscribe: {}", e); + } } /// Get all gift wrap events in the messaging relays for a given public key - async fn get_messages( - client: &Client, - urls: &[RelayUrl], - public_key: PublicKey, - ) -> Result<(), Error> { + pub async fn get_messages(client: &Client, public_key: PublicKey, urls: &[RelayUrl]) { + // Verify that there are relays provided + if urls.is_empty() { + return; + } + + // Ensure relay connection + for url in urls.iter() { + client.add_relay(url).await.ok(); + client.connect_relay(url).await.ok(); + } + let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); - // Verify that there are relays provided - if urls.is_empty() { - return Err(anyhow!("No relays provided")); - } - - // Add and connect relays - for url in urls { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } + // Unsubscribe from the previous subscription + client.unsubscribe(&id).await; // Subscribe to filters to user's messaging relays - client.subscribe_with_id_to(urls, id, filter, None).await?; - - Ok(()) + if let Err(e) = client.subscribe_with_id_to(urls, id, filter, None).await { + log::error!("Failed to subscribe: {}", e); + } else { + log::info!("Subscribed to gift wrap events for public key {public_key}",); + } } /// Get metadata for a list of public keys async fn get_metadata_for_list(client: &Client, pubkeys: Vec) -> Result<(), Error> { let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; + let kinds = vec![Kind::Metadata, Kind::ContactList]; // Return if the list is empty if pubkeys.is_empty() { @@ -290,7 +288,7 @@ impl NostrRegistry { } let filter = Filter::new() - .limit(pubkeys.len() * kinds.len() + 10) + .limit(pubkeys.len() * kinds.len()) .authors(pubkeys) .kinds(kinds); @@ -302,6 +300,19 @@ impl NostrRegistry { Ok(()) } + fn extract_write_relays(event: &Event) -> Vec { + nip65::extract_relay_list(event) + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == &Some(RelayMetadata::Write) { + Some(url.to_owned()) + } else { + None + } + }) + .take(3) + .collect() + } + /// Extract an encryption keys announcement from an event. pub fn extract_announcement(event: &Event) -> Result { let public_key = event @@ -342,8 +353,8 @@ impl NostrRegistry { } /// Returns a reference to the nostr client. - pub fn client(&self) -> Arc { - Arc::clone(&self.client) + pub fn client(&self) -> Client { + self.client.clone() } /// Returns a reference to the event tracker. @@ -352,7 +363,7 @@ impl NostrRegistry { } /// Returns a reference to the cache manager. - pub fn cache_manager(&self) -> Arc> { - Arc::clone(&self.cache_manager) + pub fn gossip(&self) -> Arc> { + Arc::clone(&self.gossip) } } diff --git a/crates/state/src/storage.rs b/crates/state/src/storage.rs index ba5ebd6a..978ae08a 100644 --- a/crates/state/src/storage.rs +++ b/crates/state/src/storage.rs @@ -3,6 +3,8 @@ use std::collections::{HashMap, HashSet}; use gpui::SharedString; use nostr_sdk::prelude::*; +use crate::NostrRegistry; + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Announcement { id: EventId, @@ -56,32 +58,129 @@ impl Response { } #[derive(Debug, Clone, Default)] -pub struct CacheManager { - /// Cache of messaging relays for each public key - relay: HashMap>, +pub struct Gossip { + /// Gossip relays for each public key + relays: HashMap)>>, - /// Cache of device announcement for each public key - announcement: HashMap>, + /// Messaging relays for each public key + messaging_relays: HashMap>, + + /// Encryption announcement for each public key + announcements: HashMap>, } -impl CacheManager { - pub fn relay(&self, public_key: &PublicKey) -> Option<&HashSet> { - self.relay.get(public_key) +impl Gossip { + /// Get inbox relays for a public key + pub fn inbox_relays(&self, public_key: &PublicKey) -> Vec { + self.relays + .get(public_key) + .map(|relays| { + relays + .iter() + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == &Some(RelayMetadata::Write) { + Some(url.to_owned()) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() } - pub fn insert_relay(&mut self, public_key: PublicKey, urls: Vec) { - self.relay.entry(public_key).or_default().extend(urls); + /// Get outbox relays for a public key + pub fn outbox_relays(&self, public_key: &PublicKey) -> Vec { + self.relays + .get(public_key) + .map(|relays| { + relays + .iter() + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == &Some(RelayMetadata::Read) { + Some(url.to_owned()) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() } - pub fn announcement(&self, public_key: &PublicKey) -> Option<&Option> { - self.announcement.get(public_key) + /// Insert gossip relays for a public key + pub fn insert_relays(&mut self, event: &Event) { + self.relays.entry(event.pubkey).or_default().extend( + event + .tags + .iter() + .filter_map(|tag| { + if let Some(TagStandard::RelayMetadata { + relay_url, + metadata, + }) = tag.clone().to_standardized() + { + Some((relay_url, metadata)) + } else { + None + } + }) + .take(3), + ); } - pub fn insert_announcement( - &mut self, - public_key: PublicKey, - announcement: Option, - ) { - self.announcement.insert(public_key, announcement); + /// Get messaging relays for a public key + pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec { + self.messaging_relays + .get(public_key) + .cloned() + .unwrap_or_default() + .into_iter() + .collect() + } + + /// Insert messaging relays for a public key + pub fn insert_messaging_relays(&mut self, event: &Event) { + self.messaging_relays + .entry(event.pubkey) + .or_default() + .extend( + event + .tags + .iter() + .filter_map(|tag| { + if let Some(TagStandard::Relay(url)) = tag.as_standardized() { + Some(url.to_owned()) + } else { + None + } + }) + .take(3), + ); + } + + /// Ensure connections for the given relay list + pub async fn ensure_connections(&self, client: &Client, urls: &[RelayUrl]) { + for url in urls { + client.add_relay(url).await.ok(); + client.connect_relay(url).await.ok(); + } + } + + /// Get announcement for a public key + pub fn announcement(&self, public_key: &PublicKey) -> Option { + self.announcements + .get(public_key) + .cloned() + .unwrap_or_default() + } + + /// Insert announcement for a public key + pub fn insert_announcement(&mut self, event: &Event) { + let announcement = NostrRegistry::extract_announcement(event).ok(); + + self.announcements + .entry(event.pubkey) + .or_insert(announcement); } } diff --git a/crates/state/src/tracker.rs b/crates/state/src/tracker.rs index b44ba800..739265d0 100644 --- a/crates/state/src/tracker.rs +++ b/crates/state/src/tracker.rs @@ -11,9 +11,6 @@ pub fn initialized_at() -> &'static Timestamp { #[derive(Debug, Clone, Default)] pub struct EventTracker { - /// Tracking events that have failed to unwrap - pub failed_unwrap_events: Vec, - /// Tracking events that have been resent by Coop in the current session pub resent_ids: Vec>, @@ -28,10 +25,6 @@ pub struct EventTracker { } impl EventTracker { - pub fn failed_unwrap_events(&self) -> &Vec { - &self.failed_unwrap_events - } - pub fn resent_ids(&self) -> &Vec> { &self.resent_ids }