From eb5d64a3afe07136d1958a763072d6f79263c8c1 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 18 May 2026 08:07:59 +0700 Subject: [PATCH] . --- Cargo.lock | 13 -- crates/chat/src/lib.rs | 109 ++++----- crates/chat/src/room.rs | 22 +- crates/device/src/lib.rs | 62 +++-- crates/state/Cargo.toml | 1 - crates/state/src/gossip.rs | 73 ++++++ crates/state/src/lib.rs | 311 ++++++++++++++++--------- crates/state/src/signer.rs | 19 +- desktop/src/dialogs/accounts.rs | 29 +-- desktop/src/panels/contact_list.rs | 6 +- desktop/src/panels/greeter.rs | 25 +- desktop/src/panels/messaging_relays.rs | 6 +- desktop/src/panels/profile.rs | 7 +- desktop/src/panels/relay_list.rs | 4 +- desktop/src/workspace.rs | 170 +++++++------- 15 files changed, 524 insertions(+), 333 deletions(-) create mode 100644 crates/state/src/gossip.rs diff --git a/Cargo.lock b/Cargo.lock index 712dae5..5204074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4483,18 +4483,6 @@ dependencies = [ "nostr", ] -[[package]] -name = "nostr-gossip-memory" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#919b2cdd1a1909b2082911a5fff23cfbff22b8fd" -dependencies = [ - "indexmap", - "lru", - "nostr", - "nostr-gossip", - "tokio", -] - [[package]] name = "nostr-lmdb" version = "0.44.0" @@ -6691,7 +6679,6 @@ dependencies = [ "nostr", "nostr-blossom", "nostr-connect", - "nostr-gossip-memory", "nostr-lmdb", "nostr-sdk", "petname", diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 77a32a4..59523a9 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -134,41 +134,21 @@ impl ChatRegistry { subscriptions.push( // Subscribe to the signer event - cx.subscribe_in(&nostr, window, |this, state, event, window, cx| { - if event == &StateEvent::SignerSet { + cx.subscribe(&nostr, |this, _state, event, cx| { + if let StateEvent::SignerSet(public_key) = event { this.reset(cx); - this.get_contact_list(cx); + this.get_contact_list(public_key, cx); this.get_rooms(cx); - - let signer = state.read(cx).signer(); - cx.spawn_in(window, async move |this, cx| { - let user_signer = signer.get().await; - this.update(cx, |this, cx| { - this.get_messages(user_signer, cx); - }) - .ok(); - }) - .detach(); + this.get_messages(public_key, cx); }; }), ); subscriptions.push( // Subscribe to the device event - cx.subscribe_in(&device, window, |_this, _s, event, window, cx| { - if event == &DeviceEvent::Set { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - cx.spawn_in(window, async move |this, cx| { - if let Some(device_signer) = signer.get_encryption_signer().await { - this.update(cx, |this, cx| { - this.get_messages(device_signer, cx); - }) - .ok(); - } - }) - .detach(); + cx.subscribe(&device, |this, _device, event, cx| { + if let DeviceEvent::Set(public_key) = event { + this.get_messages(public_key, cx); }; }), ); @@ -342,17 +322,17 @@ impl ChatRegistry { } /// Get contact list from relays - fn get_contact_list(&mut self, cx: &mut Context) { + fn get_contact_list(&mut self, public_key: &PublicKey, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { - return; - }; + let public_key = public_key.to_owned(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); let task: Task> = cx.background_spawn(async move { + let urls = write_relays.await; let id = SubscriptionId::new("contact-list"); + let opts = SubscribeAutoCloseOptions::default() .exit_policy(ReqExitPolicy::ExitOnEOSE) .timeout(Some(Duration::from_secs(TIMEOUT))); @@ -363,8 +343,13 @@ impl ChatRegistry { .author(public_key) .limit(1); + let target = urls + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe - client.subscribe(filter).close_on(opts).with_id(id).await?; + client.subscribe(target).close_on(opts).with_id(id).await?; Ok(()) }); @@ -373,11 +358,11 @@ impl ChatRegistry { } /// Get all messages for the provided signer - fn get_messages(&mut self, signer: Arc, cx: &mut Context) { - let task = self.subscribe_gift_wrap_events(signer, cx); + fn get_messages(&mut self, public_key: &PublicKey, cx: &mut Context) { + let future = self.subscribe_msg(public_key, cx); self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { + match future.await { Ok(_) => { this.update(cx, |this, cx| { this.set_initializing(false, cx); @@ -399,9 +384,12 @@ impl ChatRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; let id = SubscriptionId::new("inbox-relay"); + let urls = write_relays.await; // Construct filter for inbox relays let filter = Filter::new() @@ -409,9 +397,14 @@ impl ChatRegistry { .author(public_key) .limit(1); + let target = urls + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Stream events from user's write relays let mut stream = client - .stream_events(filter) + .stream_events(target) .with_id(id) .timeout(Duration::from_secs(TIMEOUT)) .await?; @@ -428,18 +421,14 @@ impl ChatRegistry { } /// Continuously get gift wrap events for the signer - fn subscribe_gift_wrap_events( - &self, - signer: Arc, - cx: &App, - ) -> Task> { + fn subscribe_msg(&self, public_key: &PublicKey, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let urls = self.get_messaging_relays(cx); + let public_key = public_key.to_owned(); cx.background_spawn(async move { let urls = urls.await?; - let public_key = signer.get_public_key().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); @@ -468,26 +457,30 @@ impl ChatRegistry { /// Refresh the chat registry, fetching messages and contact list from relays. pub fn refresh(&mut self, window: &mut Window, cx: &mut Context) { self.reset(cx); - self.get_contact_list(cx); self.get_rooms(cx); let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); - cx.spawn_in(window, async move |this, cx| { + self.tasks.push(cx.spawn_in(window, async move |this, cx| { let user_signer = signer.get().await; - let device_signer = signer.get_encryption_signer().await; + let user_pubkey = user_signer.get_public_key().await?; this.update(cx, |this, cx| { - this.get_messages(user_signer, cx); + this.get_messages(&user_pubkey, cx); + })?; - if let Some(device_signer) = device_signer { - this.get_messages(device_signer, cx); - } - }) - .ok(); - }) - .detach(); + let device_signer = signer.get_encryption_signer().await; + + if let Some(device_signer) = device_signer { + let device_pubkey = device_signer.get_public_key().await?; + this.update(cx, |this, cx| { + this.get_messages(&device_pubkey, cx); + })?; + } + + Ok(()) + })); } /// Set the initializing status of the chat registry @@ -791,14 +784,12 @@ impl ChatRegistry { pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); match self.rooms.iter().find(|e| e.read(cx).id == message.room) { Some(room) => { room.update(cx, |this, cx| { - if this.kind == RoomKind::Request - && let Some(public_key) = signer.public_key() - && message.rumor.pubkey == public_key - { + if this.kind == RoomKind::Request && message.rumor.pubkey == public_key { this.set_ongoing(cx); } this.push_message(message, cx); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 16e58ca..f13147f 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -10,7 +10,8 @@ use itertools::Itertools; use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; use settings::{RoomConfig, SignerKind}; -use state::{CoopSigner, NostrRegistry, TIMEOUT}; +use smol::lock::RwLock; +use state::{CoopSigner, Gossip, NostrRegistry, TIMEOUT}; use crate::NewMessage; @@ -427,7 +428,7 @@ impl Room { let nostr = NostrRegistry::global(cx); // Get current user's public key - let sender = nostr.read(cx).signer().public_key()?; + let sender = nostr.read(cx).signer().public_key(); // Get all members, excluding the sender let members: Vec = self @@ -477,10 +478,11 @@ impl Room { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let gossip = nostr.read(cx).gossip(); let signer = nostr.read(cx).signer(); // Get current user's public key - let public_key = nostr.read(cx).signer().public_key()?; + let public_key = nostr.read(cx).signer().public_key(); let sender = persons.read(cx).get(&public_key, cx); let config = self.config.clone(); @@ -529,7 +531,7 @@ impl Room { }; // Send the gift wrap event and collect the report - match send_gift_wrap(&client, &signer, &member, &rumor, use_encryption).await { + match send(&client, &gossip, &signer, &member, &rumor, use_encryption).await { Ok(report) => { reports.push(report); sents += 1; @@ -552,7 +554,7 @@ impl Room { SignerKind::User => false, }; - match send_gift_wrap(&client, &signer, &sender, &rumor, use_encryption).await { + match send(&client, &gossip, &signer, &sender, &rumor, use_encryption).await { Ok(report) => reports.push(report), Err(error) => { let report = SendReport::new(public_key).error(error.to_string()); @@ -567,8 +569,9 @@ impl Room { } // Helper function to send a gift-wrapped event -async fn send_gift_wrap( +async fn send( client: &Client, + gossip: &Arc>, signer: &Arc, receiver: &Person, rumor: &UnsignedEvent, @@ -602,10 +605,15 @@ async fn send_gift_wrap( } }; + let relays = gossip.read().await.messaging_relays(&receiver); + for url in relays.iter() { + client.add_relay(url).and_connect().await?; + } + // Send the gift wrap event and collect the report let report = client .send_event(&event) - .to_nip17() + .to(relays) .ack_policy(AckPolicy::none()) .await .map(|output| { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 99c201d..525e37f 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -12,7 +12,7 @@ use gpui::{ }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use state::{Announcement, CoopSigner, NostrRegistry, StateEvent, TIMEOUT, app_name}; +use state::{Announcement, CoopSigner, NostrRegistry, StateEvent, app_name}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::Button; @@ -35,7 +35,7 @@ impl Global for GlobalDeviceRegistry {} #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum DeviceEvent { /// A new encryption signer has been set - Set, + Set(PublicKey), /// The device is requesting an encryption key Requesting, /// The device is creating a new encryption key @@ -90,9 +90,9 @@ impl DeviceRegistry { // Subscribe to nostr state events let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| { - if event == &StateEvent::SignerSet { + if let StateEvent::SignerSet(public_key) = event { this.set_initializing(true, cx); - this.get_announcement(cx); + this.get_announcement(public_key, cx); }; }); @@ -190,12 +190,13 @@ impl DeviceRegistry { let signer = nostr.read(cx).signer(); self.tasks.push(cx.spawn(async move |this, cx| { + let public_key = new.get_public_key().await?; signer.set_encryption_signer(new).await; // Update state this.update(cx, |this, cx| { this.set_initializing(false, cx); - cx.emit(DeviceEvent::Set); + cx.emit(DeviceEvent::Set(public_key)); })?; Ok(()) @@ -219,14 +220,16 @@ impl DeviceRegistry { } /// Get device announcement for current user - pub fn get_announcement(&mut self, cx: &mut Context) { + pub fn get_announcement(&mut self, public_key: &PublicKey, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let write_relays = nostr.read(cx).write_relays(public_key, cx); let task: Task> = cx.background_spawn(async move { let signer = signer.get().await; let public_key = signer.get_public_key().await?; + let urls = write_relays.await; // Construct the filter for the device announcement event let filter = Filter::new() @@ -234,10 +237,15 @@ impl DeviceRegistry { .author(public_key) .limit(1); + let target = urls + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Stream events from user's write relays let mut stream = client - .stream_events(filter) - .timeout(Duration::from_secs(TIMEOUT)) + .stream_events(target) + .timeout(Duration::from_secs(10)) .await?; while let Some((_url, res)) = stream.next().await { @@ -373,9 +381,12 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; let id = SubscriptionId::new("dekey-requests"); + let urls = write_relays.await; // Construct a filter for encryption key requests let filter = Filter::new() @@ -383,8 +394,13 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); + let target = urls + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe to the device key requests on user's write relays - client.subscribe(vec![filter]).with_id(id).await?; + client.subscribe(target).with_id(id).await?; Ok(()) })); @@ -396,12 +412,13 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + let app_keys = nostr.read(cx).keys(); let app_pubkey = app_keys.public_key(); let task: Task, Error>> = cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; - // Construct a filter to get the latest approval event let filter = Filter::new() .kind(Kind::Custom(4455)) @@ -414,6 +431,7 @@ impl DeviceRegistry { Some(event) => Ok(Some(event)), // No approval event found, construct a request event None => { + let write_relays = write_relays.await; let signer = signer.get().await; // Construct an event for device key request @@ -430,7 +448,7 @@ impl DeviceRegistry { .await?; // Send the event to write relays - client.send_event(&event).to_nip65().await?; + client.send_event(&event).to(write_relays).await?; Ok(None) } @@ -468,8 +486,11 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let urls = write_relays.await; // Construct a filter for device key requests let filter = Filter::new() @@ -477,8 +498,13 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); + let target = urls + .into_iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); + // Subscribe to the device key requests on user's write relays - client.subscribe(filter).await?; + client.subscribe(target).await?; Ok(()) })); @@ -531,6 +557,9 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let signer_pubkey = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&signer_pubkey, cx); + // Get user's write relays let event = event.clone(); let id: SharedString = event.id.to_hex().into(); @@ -542,6 +571,7 @@ impl DeviceRegistry { let secret = keys.secret_key().to_secret_hex(); let device_signer: Arc = Arc::new(keys); + let write_relays = write_relays.await; let signer = signer.get().await; // Extract the target public key from the event tags @@ -569,7 +599,7 @@ impl DeviceRegistry { .await?; // Send the response event to the user's relay list - client.send_event(&event).to_nip65().await?; + client.send_event(&event).to(write_relays).await?; Ok(()) }); diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index f3384d7..fcedd0d 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,7 +10,6 @@ common = { path = "../common" } nostr.workspace = true nostr-sdk.workspace = true nostr-lmdb.workspace = true -nostr-gossip-memory.workspace = true nostr-connect.workspace = true nostr-blossom.workspace = true diff --git a/crates/state/src/gossip.rs b/crates/state/src/gossip.rs new file mode 100644 index 0000000..18b290b --- /dev/null +++ b/crates/state/src/gossip.rs @@ -0,0 +1,73 @@ +use std::collections::{HashMap, HashSet}; + +use nostr_sdk::prelude::*; + +#[derive(Debug, Clone, Default)] +pub struct Gossip { + pub nip17: HashMap>, + pub nip65: HashMap)>>, +} + +impl Gossip { + /// Parse and insert NIP-65 or NIP-17 relays into the gossip state. + pub fn insert(&mut self, event: &Event) { + match event.kind { + Kind::InboxRelays => { + let urls: Vec = nip17::extract_relay_list(event).take(3).collect(); + + if !urls.is_empty() { + self.nip17.entry(event.pubkey).or_default().extend(urls); + } + } + Kind::RelayList => { + let urls: Vec<(RelayUrl, Option)> = + nip65::extract_relay_list(event).collect(); + + if !urls.is_empty() { + self.nip65.entry(event.pubkey).or_default().extend(urls); + } + } + _ => {} + } + } + + /// Get all write relays for a given public key + pub fn write_relays(&self, public_key: &PublicKey) -> Vec { + self.nip65 + .get(public_key) + .map(|relays| { + relays + .iter() + .filter(|(_, metadata)| metadata.as_ref() == Some(&RelayMetadata::Write)) + .map(|(url, _)| url) + .take(3) + .cloned() + .collect() + }) + .unwrap_or_default() + } + + /// Get all read relays for a given public key + pub fn read_relays(&self, public_key: &PublicKey) -> Vec { + self.nip65 + .get(public_key) + .map(|relays| { + relays + .iter() + .filter(|(_, metadata)| metadata.as_ref() == Some(&RelayMetadata::Read)) + .map(|(url, _)| url) + .take(3) + .cloned() + .collect() + }) + .unwrap_or_default() + } + + /// Get all messaging relays for a given public key + pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec { + self.nip17 + .get(public_key) + .map(|relays| relays.iter().cloned().collect()) + .unwrap_or_default() + } +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 4d07f84..eac97d0 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -7,19 +7,21 @@ use anyhow::{Error, anyhow}; use common::config_dir; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window}; use nostr_connect::prelude::*; -use nostr_gossip_memory::prelude::*; use nostr_lmdb::prelude::*; use nostr_sdk::prelude::*; +use smol::lock::RwLock; mod blossom; mod constants; mod device; +mod gossip; mod nip05; mod signer; pub use blossom::*; pub use constants::*; pub use device::*; +pub use gossip::*; pub use nip05::*; pub use signer::*; @@ -53,7 +55,7 @@ pub enum StateEvent { /// Show the identity dialog Show, /// A new signer has been set - SignerSet, + SignerSet(PublicKey), /// An error occurred Error(SharedString), } @@ -73,6 +75,9 @@ pub struct NostrRegistry { /// Nostr client client: Client, + /// Nostr gossip implementation + gossip: Arc>, + /// Nostr signer signer: Arc, @@ -109,7 +114,7 @@ impl NostrRegistry { let key_dir = config_dir().join("keys"); let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate()); - // Construct the nostr signer + let gossip = Arc::new(RwLock::new(Gossip::default())); let signer = Arc::new(CoopSigner::new(app_keys.clone())); // Get all local stored npubs @@ -131,8 +136,8 @@ impl NostrRegistry { // Construct the nostr client let client = ClientBuilder::default() .database(lmdb) - .gossip(NostrGossipMemory::unbounded()) .connect_timeout(Duration::from_secs(10)) + .max_avg_latency(Duration::from_millis(800)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), }) @@ -141,6 +146,8 @@ impl NostrRegistry { // Run at the end of current cycle cx.defer_in(window, |this, _window, cx| { this.connect(cx); + this.handle_notifications(cx); + // Create an identity if none exists if this.npubs.read(cx).is_empty() { this.create_identity(cx); @@ -152,6 +159,7 @@ impl NostrRegistry { Self { client, + gossip, signer, npubs, key_dir, @@ -165,6 +173,11 @@ impl NostrRegistry { self.client.clone() } + /// Get the gossip instance + pub fn gossip(&self) -> Arc> { + self.gossip.clone() + } + /// Get the nostr signer pub fn signer(&self) -> Arc { self.signer.clone() @@ -207,11 +220,10 @@ impl NostrRegistry { Ok(npubs) } - /// Connect to the bootstrapping relays - fn connect(&mut self, cx: &mut Context) { + fn background_connect(&self, cx: &App) -> Task> { let client = self.client(); - let task: Task> = cx.background_spawn(async move { + cx.background_spawn(async move { // Add search relay to the relay pool for url in SEARCH_RELAYS.into_iter() { client @@ -237,20 +249,60 @@ impl NostrRegistry { client.connect().await; Ok(()) - }); + }) + } + + /// Connect to the bootstrapping relays + fn connect(&mut self, cx: &mut Context) { + let task = self.background_connect(cx); // Emit connecting event cx.emit(StateEvent::Connecting); - self.tasks.push(cx.spawn(async move |this, cx| { + // Spawn a task to handle the connection result + cx.spawn(async move |this, cx| { if let Err(e) = task.await { this.update(cx, |_this, cx| { cx.emit(StateEvent::error(e.to_string())); - })?; + }) + .ok(); } else { this.update(cx, |_this, cx| { cx.emit(StateEvent::Connected); - })?; + }) + .ok(); + } + }) + .detach(); + } + + fn handle_notifications(&mut self, cx: &App) { + let client = self.client(); + let gossip = self.gossip.clone(); + + self.tasks.push(cx.background_spawn(async move { + let mut notifications = client.notifications(); + let mut processed_events = HashSet::new(); + + while let Some(notification) = notifications.next().await { + if let ClientNotification::Message { message, .. } = notification + && let RelayMessage::Event { event, .. } = *message + { + if !processed_events.insert(event.id) { + // Skip if the event has already been processed + continue; + } + + match event.kind { + Kind::RelayList => { + gossip.write().await.insert(&event); + } + Kind::InboxRelays => { + gossip.write().await.insert(&event); + } + _ => {} + } + } } Ok(()) @@ -264,6 +316,7 @@ impl NostrRegistry { let npub = public_key.to_bech32().unwrap(); let key_path = self.key_dir.join(format!("{}.npub", npub)); + let app_keys = self.app_keys.clone(); let app_signer: Arc = Arc::new(app_keys.clone()); @@ -274,9 +327,18 @@ impl NostrRegistry { if let Ok(secret) = SecretKey::parse(&decrypted) { let keys = Keys::new(secret); - cx.spawn(async move |_cx| { + let public_key = keys.public_key(); + + cx.spawn(async move |cx| { signer.switch(keys).await; client.unsubscribe_all().await.ok(); + get_gossip_relays(&client, &public_key).await.ok(); + + cx.update_global::(|this, cx| { + this.0.update(cx, |_, cx| { + cx.emit(StateEvent::SignerSet(public_key)); + }); + }); }) .detach(); @@ -286,9 +348,29 @@ impl NostrRegistry { let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; nip46.auth_url_handler(CoopAuthUrlHandler); - cx.spawn(async move |_cx| { + cx.spawn(async move |cx| { signer.switch(nip46).await; client.unsubscribe_all().await.ok(); + + // Verify the public key + match signer.get_public_key().await { + Ok(public_key) => { + get_gossip_relays(&client, &public_key).await.ok(); + + cx.update_global::(|this, cx| { + this.0.update(cx, |_, cx| { + cx.emit(StateEvent::SignerSet(public_key)); + }); + }); + } + Err(e) => { + cx.update_global::(|this, cx| { + this.0.update(cx, |_, cx| { + cx.emit(StateEvent::error(e.to_string())); + }); + }); + } + } }) .detach(); @@ -299,33 +381,6 @@ impl NostrRegistry { Err(anyhow!("Secret not found")) } - /// Get the secret for a given npub. - pub fn get_secret(&self, public_key: PublicKey) -> Result, Error> { - let npub = public_key.to_bech32().unwrap(); - let key_path = self.key_dir.join(format!("{}.npub", npub)); - let app_keys = self.app_keys.clone(); - let app_signer: Arc = Arc::new(self.app_keys.clone()); - - if let Ok(payload) = std::fs::read_to_string(key_path) - && !payload.is_empty() - { - let decrypted = app_signer.nip44_decrypt(&public_key, &payload)?; - - if let Ok(secret) = SecretKey::parse(&decrypted) { - let keys = Keys::new(secret); - return Ok(Arc::new(keys)); - } else if let Ok(uri) = NostrConnectUri::parse(decrypted) { - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; - nip46.auth_url_handler(CoopAuthUrlHandler); - - return Ok(Arc::new(nip46)); - } - } - - Err(anyhow!("Secret not found")) - } - /// Add a new npub to the keys directory fn write_secret( &self, @@ -335,14 +390,14 @@ impl NostrRegistry { ) -> Task> { let npub = public_key.to_bech32().unwrap(); let key_path = self.key_dir.join(format!("{}.npub", npub)); - let app_keys: Arc = Arc::new(self.app_keys.clone()); + let app_signer: Arc = Arc::new(self.app_keys.clone()); cx.background_spawn(async move { // If the secret starts with "bunker://" (nostr connect), use it directly; otherwise, encrypt it let content = if secret.starts_with("bunker://") { secret } else { - app_keys.nip44_encrypt(&public_key, &secret).await? + app_signer.nip44_encrypt(&public_key, &secret).await? }; // Write the encrypted secret to the keys directory @@ -386,14 +441,35 @@ impl NostrRegistry { let task: Task> = cx.background_spawn(async move { // Construct relay list event let relay_list = default_relay_list(); - let event = EventBuilder::relay_list(relay_list) + let event = EventBuilder::relay_list(relay_list.clone()) .sign_async(&async_keys) .await?; + // Get write relays + let write_relays: Vec = relay_list + .iter() + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == &Some(RelayMetadata::Write) { + Some(url.to_owned()) + } else { + None + } + }) + .collect(); + + // Ensure the client is connected to each relay + for url in write_relays.iter() { + client + .add_relay(url) + .capabilities(RelayCapabilities::WRITE) + .and_connect() + .await?; + } + // Publish relay list client .send_event(&event) - .to(BOOTSTRAP_RELAYS) + .to(&write_relays) .ack_policy(AckPolicy::none()) .await?; @@ -408,7 +484,7 @@ impl NostrRegistry { // Publish metadata event client .send_event(&event) - .to_nip65() + .to(&write_relays) .ack_policy(AckPolicy::none()) .await?; @@ -421,7 +497,7 @@ impl NostrRegistry { // Publish contact list event client .send_event(&event) - .to_nip65() + .to(&write_relays) .ack_policy(AckPolicy::none()) .await?; @@ -432,7 +508,11 @@ impl NostrRegistry { .await?; // Publish messaging relay list event - client.send_event(&event).to_nip65().await?; + client + .send_event(&event) + .to(&write_relays) + .ack_policy(AckPolicy::none()) + .await?; // Write user's credentials to the system keyring write_secret.await?; @@ -444,7 +524,9 @@ impl NostrRegistry { match task.await { Ok(_) => { this.update(cx, |this, cx| { - this.switch_account(keys.public_key(), cx); + if let Err(e) = this.switch_account(keys.public_key(), cx) { + cx.emit(StateEvent::error(e.to_string())); + } })?; } Err(e) => { @@ -468,7 +550,9 @@ impl NostrRegistry { match write_secret.await { Ok(_) => { this.update(cx, |this, cx| { - this.switch_account(keys.public_key(), cx); + if let Err(e) = this.switch_account(keys.public_key(), cx) { + cx.emit(StateEvent::error(e.to_string())); + } })?; } Err(e) => { @@ -507,7 +591,9 @@ impl NostrRegistry { match write_secret.await { Ok(_) => { this.update(cx, |this, cx| { - this.switch_account(public_key, cx); + if let Err(e) = this.switch_account(public_key, cx) { + cx.emit(StateEvent::error(e.to_string())); + } })?; } Err(e) => { @@ -621,69 +707,64 @@ impl NostrRegistry { }) } - /// Perform a WoT (via Vertex) search for a given query. - pub fn wot_search(&self, query: &str, cx: &App) -> Task, Error>> { + pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { let client = self.client(); - let query = query.to_string(); - let signer = self.signer(); + let gossip = self.gossip.clone(); + let public_key = public_key.to_owned(); cx.background_spawn(async move { - let signer = signer.get().await; + let urls = gossip.read().await.write_relays(&public_key); - // Construct a vertex request event - let event = EventBuilder::new(Kind::Custom(5315), "") - .tags(vec![ - Tag::custom("param", vec!["search", &query]), - Tag::custom("param", vec!["limit", "10"]), - ]) - .sign_async(&signer) - .await?; - - // Send the event to vertex relays - let output = client.send_event(&event).to(WOT_RELAYS).await?; - - // Construct a filter to get the response or error from vertex - let filter = Filter::new() - .kinds(vec![Kind::Custom(6315), Kind::Custom(7000)]) - .event(output.id().to_owned()); - - // Construct target for subscription - let target: HashMap<&str, Vec> = WOT_RELAYS - .into_iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect(); - - // Stream events from the wot relays - let mut stream = client - .stream_events(target) - .timeout(Duration::from_secs(TIMEOUT)) - .await?; - - while let Some((_url, res)) = stream.next().await { - if let Ok(event) = res { - match event.kind { - Kind::Custom(6315) => { - let content: serde_json::Value = serde_json::from_str(&event.content)?; - let pubkeys: Vec = content - .as_array() - .into_iter() - .flatten() - .filter_map(|item| item.as_object()) - .filter_map(|obj| obj.get("pubkey").and_then(|v| v.as_str())) - .filter_map(|pubkey_str| PublicKey::parse(pubkey_str).ok()) - .collect(); - - return Ok(pubkeys); - } - Kind::Custom(7000) => { - return Err(anyhow!("Search error")); - } - _ => {} - } - } + // Ensure the client is connected to each relay + for url in urls.iter() { + client + .add_relay(url) + .capabilities(RelayCapabilities::WRITE) + .and_connect() + .await + .ok(); } - Err(anyhow!("No results for query: {query}")) + urls + }) + } + + pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { + let client = self.client(); + let gossip = self.gossip.clone(); + let public_key = public_key.to_owned(); + + cx.background_spawn(async move { + let urls = gossip.read().await.read_relays(&public_key); + + // Ensure the client is connected to each relay + for url in urls.iter() { + client + .add_relay(url) + .capabilities(RelayCapabilities::READ) + .and_connect() + .await + .ok(); + } + + urls + }) + } + + pub fn msg_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { + let client = self.client(); + let gossip = self.gossip.clone(); + let public_key = public_key.to_owned(); + + cx.background_spawn(async move { + let urls = gossip.read().await.messaging_relays(&public_key); + + // Ensure the client is connected to each relay + for url in urls.iter() { + client.add_relay(url).and_connect().await.ok(); + } + + urls }) } } @@ -717,6 +798,20 @@ fn get_or_init_app_keys(cx: &App) -> Result { } } +async fn get_gossip_relays(client: &Client, public_key: &PublicKey) -> Result<(), Error> { + let id = SubscriptionId::new("gossip"); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + let filter = Filter::new() + .author(*public_key) + .kind(Kind::RelayList) + .limit(1); + + client.subscribe(filter).close_on(opts).with_id(id).await?; + + Ok(()) +} + fn default_relay_list() -> Vec<(RelayUrl, Option)> { vec![ ( diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs index b0a93cc..7cf51af 100644 --- a/crates/state/src/signer.rs +++ b/crates/state/src/signer.rs @@ -10,7 +10,7 @@ pub struct CoopSigner { signer: Arc>>, /// User's signer public key - signer_pkey: Arc>>, + public_key: RwLock>, /// Specific signer for encryption purposes encryption_signer: Arc>>>, @@ -23,7 +23,7 @@ impl CoopSigner { { Self { signer: Arc::new(RwLock::new(Arc::new(signer))), - signer_pkey: Arc::new(RwLock::new(None)), + public_key: RwLock::new(None), encryption_signer: Arc::new(RwLock::new(None)), } } @@ -47,8 +47,13 @@ impl CoopSigner { /// /// Ensure to call this method after the signer has been initialized. /// Otherwise, it will panic. - pub fn public_key(&self) -> Option { - *self.signer_pkey.read_blocking() + pub fn public_key(&self) -> PublicKey { + self.public_key.read_blocking().unwrap() + } + + /// Check if the public key is present, indicating the signer is logged in. + pub fn is_logged_in(&self) -> bool { + self.public_key.read_blocking().is_some() } /// Switch the current signer to a new signer. @@ -57,11 +62,11 @@ impl CoopSigner { T: AsyncNostrSigner, { let mut signer = self.signer.write().await; - let mut signer_pkey = self.signer_pkey.write().await; + let mut public_key = self.public_key.write().await; let mut encryption_signer = self.encryption_signer.write().await; - // Update the public key - *signer_pkey = new.get_public_key().await.ok(); + // Verify the public key + *public_key = new.get_public_key().await.ok(); // Switch to the new signer *signer = Arc::new(new); diff --git a/desktop/src/dialogs/accounts.rs b/desktop/src/dialogs/accounts.rs index 0b3639a..4191a26 100644 --- a/desktop/src/dialogs/accounts.rs +++ b/desktop/src/dialogs/accounts.rs @@ -1,8 +1,7 @@ -use anyhow::Error; use gpui::prelude::FluentBuilder; use gpui::{ App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px, + SharedString, StatefulInteractiveElement, Styled, Subscription, Window, div, px, }; use nostr_sdk::prelude::*; use person::PersonRegistry; @@ -28,11 +27,8 @@ pub struct AccountSelector { /// The error message displayed when an error occurs. error: Entity>, - /// Async tasks - tasks: Vec>>, - /// Subscription to the signer events - _subscription: Option, + _event_subscription: Option, } impl AccountSelector { @@ -44,7 +40,7 @@ impl AccountSelector { let nostr = NostrRegistry::global(cx); let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| { match event { - StateEvent::SignerSet => { + StateEvent::SignerSet(_) => { window.close_all_modals(cx); window.refresh(); } @@ -58,8 +54,7 @@ impl AccountSelector { Self { logging_in, error, - tasks: vec![], - _subscription: Some(subscription), + _event_subscription: Some(subscription), } } @@ -89,20 +84,14 @@ impl AccountSelector { }) } - fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - + fn login(&mut self, public_key: PublicKey, _window: &mut Window, cx: &mut Context) { // Mark the public key as being logged in self.set_logging_in(public_key, cx); - match nostr.read(cx).switch_account(public_key, cx) { - Ok(()) => { - // - } - Err(e) => { - self.set_error(e.to_string(), cx); - } - }; + let nostr = NostrRegistry::global(cx); + if let Err(e) = nostr.read(cx).switch_account(public_key, cx) { + self.set_error(e.to_string(), cx); + } } fn remove(&mut self, public_key: PublicKey, cx: &mut Context) { diff --git a/desktop/src/panels/contact_list.rs b/desktop/src/panels/contact_list.rs index 8548b57..2a9090c 100644 --- a/desktop/src/panels/contact_list.rs +++ b/desktop/src/panels/contact_list.rs @@ -158,6 +158,9 @@ impl ContactListPanel { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + // Get contacts let contacts: Vec = self .contacts @@ -169,6 +172,7 @@ impl ContactListPanel { self.set_updating(true, cx); let task: Task> = cx.background_spawn(async move { + let write_relays = write_relays.await; let signer = signer.get().await; // Construct contact list event builder @@ -177,7 +181,7 @@ impl ContactListPanel { .await?; // Set contact list - client.send_event(&event).to_nip65().await?; + client.send_event(&event).to(write_relays).await?; Ok(()) }); diff --git a/desktop/src/panels/greeter.rs b/desktop/src/panels/greeter.rs index 97e1c37..ef0e137 100644 --- a/desktop/src/panels/greeter.rs +++ b/desktop/src/panels/greeter.rs @@ -31,21 +31,20 @@ impl GreeterPanel { fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); - if let Some(public_key) = signer.public_key() { - cx.spawn_in(window, async move |_this, cx| { - cx.update(|window, cx| { - Workspace::add_panel( - profile::init(public_key, window, cx), - DockPlacement::Right, - window, - cx, - ); - }) - .ok(); + cx.spawn_in(window, async move |_this, cx| { + cx.update(|window, cx| { + Workspace::add_panel( + profile::init(public_key, window, cx), + DockPlacement::Right, + window, + cx, + ); }) - .detach(); - } + .ok(); + }) + .detach(); } } diff --git a/desktop/src/panels/messaging_relays.rs b/desktop/src/panels/messaging_relays.rs index df04cd0..65b5c1f 100644 --- a/desktop/src/panels/messaging_relays.rs +++ b/desktop/src/panels/messaging_relays.rs @@ -172,6 +172,9 @@ impl MessagingRelayPanel { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + // Construct event tags let tags: Vec = self .relays @@ -183,6 +186,7 @@ impl MessagingRelayPanel { self.set_updating(true, cx); let task: Task> = cx.background_spawn(async move { + let write_relays = write_relays.await; let signer = signer.get().await; // Construct nip17 event builder @@ -192,7 +196,7 @@ impl MessagingRelayPanel { .await?; // Set messaging relays - client.send_event(&event).to_nip65().await?; + client.send_event(&event).to(write_relays).await?; Ok(()) }); diff --git a/desktop/src/panels/profile.rs b/desktop/src/panels/profile.rs index 008ead2..3dafdfb 100644 --- a/desktop/src/panels/profile.rs +++ b/desktop/src/panels/profile.rs @@ -208,9 +208,14 @@ impl ProfilePanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + + let public_key = signer.public_key(); + let write_relays = nostr.read(cx).write_relays(&public_key, cx); + let metadata = metadata.clone(); cx.background_spawn(async move { + let write_relays = write_relays.await; let signer = signer.get().await; // Build and sign the metadata event @@ -219,7 +224,7 @@ impl ProfilePanel { .await?; // Send event to user's relays - client.send_event(&event).await?; + client.send_event(&event).to(write_relays).await?; Ok(()) }) diff --git a/desktop/src/panels/relay_list.rs b/desktop/src/panels/relay_list.rs index 6d6e457..7db6d89 100644 --- a/desktop/src/panels/relay_list.rs +++ b/desktop/src/panels/relay_list.rs @@ -11,7 +11,7 @@ use gpui::{ use nostr_sdk::prelude::*; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; -use state::NostrRegistry; +use state::{BOOTSTRAP_RELAYS, NostrRegistry}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; @@ -219,7 +219,7 @@ impl RelayListPanel { let event = EventBuilder::relay_list(relays).sign_async(&signer).await?; // Set relay list for current user - client.send_event(&event).await?; + client.send_event(&event).to(BOOTSTRAP_RELAYS).await?; Ok(()) }); diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 64f17f4..29ab750 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -126,7 +126,7 @@ impl Workspace { window.push_notification(note, cx); } - StateEvent::SignerSet => { + StateEvent::SignerSet(_) => { this.set_center_layout(window, cx); // Clear the signer notification window.clear_notification::(cx); @@ -165,7 +165,7 @@ impl Workspace { window.push_notification(note, cx); } - DeviceEvent::Set => { + DeviceEvent::Set(_) => { let note = Notification::new() .id::() .message("Encryption Key has been set") @@ -307,17 +307,16 @@ impl Workspace { Command::ShowProfile => { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); + let public_key = signer.public_key(); - if let Some(public_key) = signer.public_key() { - self.dock.update(cx, |this, cx| { - this.add_panel( - Arc::new(profile::init(public_key, window, cx)), - DockPlacement::Right, - window, - cx, - ); - }); - } + self.dock.update(cx, |this, cx| { + this.add_panel( + Arc::new(profile::init(public_key, window, cx)), + DockPlacement::Right, + window, + cx, + ); + }); } Command::ShowContactList => { self.dock.update(cx, |this, cx| { @@ -368,8 +367,11 @@ impl Workspace { } Command::RefreshEncryption => { let device = DeviceRegistry::global(cx); + let nostr = NostrRegistry::global(cx); + let public_key = nostr.read(cx).signer().public_key(); + device.update(cx, |this, cx| { - this.get_announcement(cx); + this.get_announcement(&public_key, cx); }); } Command::ResetEncryption => { @@ -561,80 +563,78 @@ impl Workspace { fn titlebar_left(&mut self, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); - let current_user = signer.public_key(); + let is_logged_in = signer.is_logged_in(); + + if !is_logged_in { + return div(); + } + + let persons = PersonRegistry::global(cx); + let public_key = signer.public_key(); + + let profile = persons.read(cx).get(&public_key, cx); + let avatar = profile.avatar(); + let name = profile.name(); h_flex() .flex_shrink_0() .gap_2() - .when_none(¤t_user, |this| { - this.child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from("Choose an account to continue...")), - ) - }) - .when_some(current_user.as_ref(), |this, public_key| { - let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(public_key, cx); - let avatar = profile.avatar(); - let name = profile.name(); + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Choose an account to continue...")), + ) + .child( + Button::new("current-user") + .child(Avatar::new(avatar.clone()).xsmall()) + .small() + .caret() + .compact() + .transparent() + .dropdown_menu(move |this, _window, _cx| { + let avatar = avatar.clone(); + let name = name.clone(); - this.child( - Button::new("current-user") - .child(Avatar::new(avatar.clone()).xsmall()) - .small() - .caret() - .compact() - .transparent() - .dropdown_menu(move |this, _window, _cx| { - let avatar = avatar.clone(); - let name = name.clone(); - - this.min_w(px(256.)) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1p5() - .text_xs() - .text_color(cx.theme().text_muted) - .child(Avatar::new(avatar.clone()).xsmall()) - .child(name.clone()) - })) - .separator() - .menu_with_icon( - "Profile", - IconName::Profile, - Box::new(Command::ShowProfile), - ) - .menu_with_icon( - "Contact List", - IconName::Book, - Box::new(Command::ShowContactList), - ) - .menu_with_icon( - "Backup", - IconName::UserKey, - Box::new(Command::ShowBackup), - ) - .menu_with_icon( - "Themes", - IconName::Sun, - Box::new(Command::ToggleTheme), - ) - .separator() - .menu_with_icon( - "Accounts", - IconName::Group, - Box::new(Command::ToggleAccount), - ) - .menu_with_icon( - "Settings", - IconName::Settings, - Box::new(Command::ShowSettings), - ) - }), - ) - }) + this.min_w(px(256.)) + .item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1p5() + .text_xs() + .text_color(cx.theme().text_muted) + .child(Avatar::new(avatar.clone()).xsmall()) + .child(name.clone()) + })) + .separator() + .menu_with_icon( + "Profile", + IconName::Profile, + Box::new(Command::ShowProfile), + ) + .menu_with_icon( + "Contact List", + IconName::Book, + Box::new(Command::ShowContactList), + ) + .menu_with_icon( + "Backup", + IconName::UserKey, + Box::new(Command::ShowBackup), + ) + .menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme)) + .separator() + .menu_with_icon( + "Accounts", + IconName::Group, + Box::new(Command::ToggleAccount), + ) + .menu_with_icon( + "Settings", + IconName::Settings, + Box::new(Command::ShowSettings), + ) + }), + ) } fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { @@ -647,12 +647,14 @@ impl Workspace { let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); + let is_logged_in = signer.is_logged_in(); - let Some(public_key) = signer.public_key() else { + if !is_logged_in { return div(); - }; + } let persons = PersonRegistry::global(cx); + let public_key = signer.public_key(); let profile = persons.read(cx).get(&public_key, cx); let announcement = profile.announcement();