diff --git a/Cargo.lock b/Cargo.lock index f430e0b..5c039d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,25 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "account" -version = "0.3.0" -dependencies = [ - "anyhow", - "common", - "gpui", - "log", - "nostr-sdk", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "state", - "theme", - "ui", -] - [[package]] name = "adler2" version = "2.0.1" @@ -1025,10 +1006,8 @@ dependencies = [ name = "chat" version = "0.3.0" dependencies = [ - "account", "anyhow", "common", - "encryption", "flume", "futures", "fuzzy-matcher", @@ -1049,12 +1028,10 @@ dependencies = [ name = "chat_ui" version = "0.3.0" dependencies = [ - "account", "anyhow", "chat", "common", "emojis", - "encryption", "gpui", "gpui_tokio", "indexset", @@ -1308,15 +1285,12 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" name = "coop" version = "0.3.0" dependencies = [ - "account", "anyhow", "assets", "auto_update", "chat", "chat_ui", "common", - "encryption", - "encryption_ui", "futures", "gpui", "gpui_tokio", @@ -1825,49 +1799,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "encryption" -version = "0.3.0" -dependencies = [ - "account", - "anyhow", - "common", - "flume", - "futures", - "gpui", - "log", - "nostr-sdk", - "serde", - "serde_json", - "smallvec", - "smol", - "state", -] - -[[package]] -name = "encryption_ui" -version = "0.3.0" -dependencies = [ - "account", - "anyhow", - "common", - "encryption", - "futures", - "gpui", - "itertools 0.13.0", - "log", - "nostr-sdk", - "person", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "state", - "theme", - "ui", -] - [[package]] name = "endi" version = "1.1.1" @@ -5198,6 +5129,7 @@ version = "0.3.0" dependencies = [ "anyhow", "common", + "flume", "gpui", "log", "nostr-sdk", @@ -6080,6 +6012,21 @@ dependencies = [ [[package]] name = "state" version = "0.3.0" +dependencies = [ + "anyhow", + "common", + "flume", + "gpui", + "log", + "nostr-lmdb", + "nostr-sdk", + "rustls", + "smol", +] + +[[package]] +name = "state_old" +version = "0.3.0" dependencies = [ "anyhow", "common", diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml deleted file mode 100644 index a594b16..0000000 --- a/crates/account/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "account" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -state = { path = "../state" } -settings = { path = "../settings" } -common = { path = "../common" } -theme = { path = "../theme" } -ui = { path = "../ui" } - -gpui.workspace = true -nostr-sdk.workspace = true - -anyhow.workspace = true -smallvec.workspace = true -smol.workspace = true -log.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs deleted file mode 100644 index 92a088b..0000000 --- a/crates/account/src/lib.rs +++ /dev/null @@ -1,208 +0,0 @@ -use std::time::Duration; - -use anyhow::Error; -use common::BOOTSTRAP_RELAYS; -use gpui::{App, AppContext, Context, Entity, Global, Task}; -use nostr_sdk::prelude::*; -use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; - -pub fn init(cx: &mut App) { - Account::set_global(cx.new(Account::new), cx); -} - -struct GlobalAccount(Entity); - -impl Global for GlobalAccount {} - -pub struct Account { - /// The public key of the account - public_key: Option, - - /// Status of the current user NIP-65 relays - pub nip65_status: Entity, - - /// Status of the current user NIP-17 relays - pub nip17_status: Entity, - - /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 2]>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum RelayStatus { - #[default] - Initial, - NotSet, - Set, -} - -impl Account { - /// Retrieve the global account state - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - /// Check if the global account state exists - pub fn has_global(cx: &App) -> bool { - cx.has_global::() - } - - /// Remove the global account state - pub fn remove_global(cx: &mut App) { - cx.remove_global::(); - } - - /// Set the global account instance - fn set_global(state: Entity, cx: &mut App) { - cx.set_global(GlobalAccount(state)); - } - - /// Create a new account instance - fn new(cx: &mut Context) -> Self { - 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(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") - } - }), - ); - - Self { - public_key: None, - nip65_status, - nip17_status, - _tasks: tasks, - } - } - - /// Observe the signer and return the public key when it sets - async fn observe_signer(client: &Client) -> Option { - let loop_duration = Duration::from_millis(800); - - loop { - if let Ok(signer) = client.signer().await { - if let Ok(public_key) = signer.get_public_key().await { - // Get current user's gossip relays - Self::get_gossip_relays(client, public_key).await.ok()?; - - return Some(public_key); - } - } - smol::Timer::after(loop_duration).await; - } - } - - /// Get gossip relays for a given public key - async fn get_gossip_relays(client: &Client, public_key: PublicKey) -> Result<(), Error> { - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - - let filter = Filter::new() - .kind(Kind::RelayList) - .author(public_key) - .limit(1); - - // Subscribe to events from the bootstrapping relays - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await?; - - Ok(()) - } - - /// Ensure the user has NIP-65 relays - async fn ensure_nip65_relays(client: &Client, public_key: PublicKey) -> Result { - let filter = Filter::new() - .kind(Kind::RelayList) - .author(public_key) - .limit(1); - - // Count the number of nip65 relays event in the database - let total = client.database().count(filter).await.unwrap_or(0); - - Ok(total > 0) - } - - /// Ensure the user has NIP-17 relays - async fn ensure_nip17_relays(client: &Client, public_key: PublicKey) -> Result { - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - // Count the number of nip17 relays event in the database - let total = client.database().count(filter).await.unwrap_or(0); - - Ok(total > 0) - } - - /// Set the public key of the account - pub fn set_account(&mut self, public_key: PublicKey, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - // Update account's public key - self.public_key = Some(public_key); - - // Add background task - self._tasks.push( - // Verify user's nip65 and nip17 relays - cx.spawn(async move |this, cx| { - 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.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") - }), - ); - - cx.notify(); - } - - /// Check if the account entity has a public key - pub fn has_account(&self) -> bool { - self.public_key.is_some() - } - - /// Get the public key of the account - pub fn public_key(&self) -> PublicKey { - // This method is only called when user is logged in, so unwrap safely - self.public_key.unwrap() - } -} diff --git a/crates/chat/Cargo.toml b/crates/chat/Cargo.toml index 9dbb715..0c2a465 100644 --- a/crates/chat/Cargo.toml +++ b/crates/chat/Cargo.toml @@ -7,8 +7,6 @@ publish.workspace = true [dependencies] common = { path = "../common" } state = { path = "../state" } -account = { path = "../account" } -encryption = { path = "../encryption" } person = { path = "../person" } settings = { path = "../settings" } diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 4262913..c36accc 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -5,20 +5,18 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; -use account::Account; use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::{EventUtils, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT}; -use encryption::Encryption; use flume::Sender; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; -use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task}; +use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task}; pub use message::*; use nostr_sdk::prelude::*; pub use room::*; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; -use state::{initialized_at, NostrRegistry, GIFTWRAP_SUBSCRIPTION}; +use state::{tracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION}; mod message; mod room; @@ -35,16 +33,10 @@ impl Global for GlobalChatRegistry {} #[derive(Debug)] pub struct ChatRegistry { /// Collection of all chat rooms - pub rooms: Vec>, + rooms: Vec>, /// Loading status of the registry - pub loading: bool, - - /// Async task for handling notifications - handle_notifications: Task<()>, - - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, + loading: bool, /// Tasks for asynchronous operations _tasks: SmallVec<[Task<()>; 4]>, @@ -79,53 +71,30 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(cx: &mut Context) -> Self { - let encryption = Encryption::global(cx); - let encryption_key = encryption.read(cx).encryption.clone(); - let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + // A flag to indicate if the registry is loading let status = Arc::new(AtomicBool::new(true)); + + // Channel for communication between nostr and gpui 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![]; - subscriptions.push( - // Observe the encryption global state - cx.observe(&encryption_key, { + tasks.push( + // Handle nostr notifications + cx.background_spawn({ + let client = client.clone(); let status = Arc::clone(&status); let tx = tx.clone(); - move |this, state, cx| { - if let Some(signer) = state.read(cx).clone() { - 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(); - } - } + async move { Self::handle_notifications(&client, &status, &tx).await } }), ); tasks.push( - // Handle unwrapping status + // Handle unwrapping progress cx.background_spawn( async move { Self::handle_unwrapping(&client, &status, &tx).await }, ), @@ -140,20 +109,20 @@ impl ChatRegistry { this.update(cx, |this, cx| { this.new_message(message, cx); }) - .expect("Entity has been released"); + .ok(); } Signal::Eose => { this.update(cx, |this, cx| { this.get_rooms(cx); }) - .expect("Entity has been released"); + .ok(); } Signal::Loading(status) => { this.update(cx, |this, cx| { this.set_loading(status, cx); this.get_rooms(cx); }) - .expect("Entity has been released"); + .ok(); } }; } @@ -163,21 +132,12 @@ impl ChatRegistry { Self { rooms: vec![], loading: true, - handle_notifications, - _subscriptions: subscriptions, _tasks: tasks, } } - async fn handle_notifications( - client: &Client, - signer: &Option, - tx: &Sender, - status: &Arc, - ) where - T: NostrSigner, - { - let initialized_at = initialized_at(); + async fn handle_notifications(client: &Client, loading: &Arc, tx: &Sender) { + let initialized_at = Timestamp::now(); let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); let mut notifications = client.notifications(); @@ -203,13 +163,13 @@ impl ChatRegistry { } // Extract the rumor from the gift wrap event - match Self::extract_rumor(client, signer, event.as_ref()).await { + match Self::extract_rumor(client, event.as_ref()).await { Ok(rumor) => { // Get all public keys public_keys.extend(rumor.all_pubkeys()); let limit_reached = public_keys.len() >= METADATA_BATCH_LIMIT; - let done = !status.load(Ordering::Acquire) && !public_keys.is_empty(); + let done = !loading.load(Ordering::Acquire) && !public_keys.is_empty(); // Get metadata for all public keys if the limit is reached if limit_reached || done { @@ -218,17 +178,24 @@ impl ChatRegistry { Self::get_metadata(client, public_keys).await.ok(); } - match &rumor.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); + let sent_by_coop = { + let tracker = tracker().read().await; + tracker.is_sent_by_coop(&event.id) + }; - if let Err(e) = tx.send_async(signal).await { - log::error!("Failed to send signal: {}", e); + if !sent_by_coop { + 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); + loading.store(true, Ordering::Release); } } } @@ -530,7 +497,7 @@ impl ChatRegistry { pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { let id = message.rumor.uniq_id(); let author = message.rumor.pubkey; - let account = Account::global(cx); + let nostr = NostrRegistry::global(cx); if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { let is_new_event = message.rumor.created_at > room.read(cx).created_at; @@ -544,7 +511,7 @@ impl ChatRegistry { } // Set this room is ongoing if the new message is from current user - if author == account.read(cx).public_key() { + if author == nostr.read(cx).identity(cx).public_key() { this.set_ongoing(cx); } @@ -566,21 +533,14 @@ impl ChatRegistry { } // Unwraps a gift-wrapped event and processes its contents. - async fn extract_rumor( - client: &Client, - signer: &Option, - gift_wrap: &Event, - ) -> Result - where - T: NostrSigner, - { + async fn extract_rumor(client: &Client, gift_wrap: &Event) -> Result { // 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, signer, gift_wrap).await?; + let unwrapped = Self::try_unwrap(client, gift_wrap).await?; let mut rumor_unsigned = unwrapped.rumor; // Generate event id for the rumor if it doesn't have one @@ -593,39 +553,7 @@ impl ChatRegistry { } // Helper method to try unwrapping with different signers - 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, - }); - } - } - + async fn try_unwrap(client: &Client, gift_wrap: &Event) -> Result { let signer = client.signer().await?; let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?; diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 0ad7878..7e4b144 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -3,43 +3,16 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::time::Duration; -use account::Account; -use anyhow::{anyhow, Error}; +use 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; +use state::{tracker, NostrRegistry}; const SEND_RETRY: usize = 10; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct SendOptions { - pub backup: bool, - pub signer_kind: SignerKind, -} - -impl SendOptions { - pub fn new() -> Self { - Self { - backup: true, - signer_kind: SignerKind::default(), - } - } - - pub fn backup(&self) -> bool { - self.backup - } -} - -impl Default for SendOptions { - fn default() -> Self { - Self::new() - } -} - #[derive(Debug, Clone)] pub struct SendReport { pub receiver: PublicKey, @@ -248,6 +221,19 @@ impl Room { self.members.clone() } + /// Returns the members of the room with their messaging relays + pub fn members_with_relays(&self, cx: &App) -> Vec<(PublicKey, Vec)> { + let nostr = NostrRegistry::global(cx); + let mut result = vec![]; + + for member in self.members.iter() { + let messaging_relays = nostr.read(cx).messaging_relays(member, cx); + result.push((member.to_owned(), messaging_relays)); + } + + result + } + /// Checks if the room has more than two members (group) pub fn is_group(&self) -> bool { self.members.len() > 2 @@ -276,8 +262,8 @@ impl Room { /// Display member is always different from the current user. pub fn display_member(&self, cx: &App) -> Profile { let persons = PersonRegistry::global(cx); - let account = Account::global(cx); - let public_key = account.read(cx).public_key(); + let nostr = NostrRegistry::global(cx); + let public_key = nostr.read(cx).identity(cx).public_key(); let target_member = self .members @@ -381,12 +367,9 @@ 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 gossip = nostr.read(cx).gossip(); - let read_gossip = gossip.read_blocking(); // Get current user - let account = Account::global(cx); - let public_key = account.read(cx).public_key(); + let public_key = nostr.read(cx).identity(cx).public_key(); // Get room's subject let subject = self.subject.clone(); @@ -398,7 +381,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 = read_gossip.messaging_relays(member).first().cloned(); + let relay_url = nostr.read(cx).relay_hint(member, cx); // Construct a public key tag with relay hint let tag = TagStandard::PublicKey { @@ -449,98 +432,63 @@ impl Room { pub fn send_message( &self, rumor: &UnsignedEvent, - opts: &SendOptions, cx: &App, ) -> Task, Error>> { - let encryption = Encryption::global(cx); - let encryption_key = encryption.read(cx).encryption_key(cx); - let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let gossip = nostr.read(cx).gossip(); - let tracker = nostr.read(cx).tracker(); + + // Get current user's public key and relays + let current_user = nostr.read(cx).identity(cx).public_key(); + let current_user_relays = nostr.read(cx).messaging_relays(¤t_user, cx); let rumor = rumor.to_owned(); - let opts = opts.to_owned(); - // Get all members - let mut members = self.members(); + // Get all members and their messaging relays + let mut members = self.members_with_relays(cx); cx.background_spawn(async move { - let signer_kind = opts.signer_kind; - 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() { - signer.get_public_key().await.ok() - } else { - None - }; + let signer = client.signer().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); - - // Determine the signer will be used based on the provided options - let signer = Self::select_signer(&opts.signer_kind, user_signer, encryption_key)?; + members.retain(|(this, _)| this != ¤t_user); // Collect the send reports let mut reports: Vec = vec![]; - for member in members.into_iter() { - // Get user's messaging relays - let urls = gossip.messaging_relays(&member); - // Get user's encryption public key if available - let encryption = gossip.announcement(&member).map(|a| a.public_key()); - + for (receiver, relays) in members.into_iter() { // Check if there are any relays to send the message to - if urls.is_empty() { - reports.push(SendReport::new(member).relays_not_found()); + if relays.is_empty() { + reports.push(SendReport::new(receiver).relays_not_found()); continue; } - // 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; + // Ensure relay connection + for url in relays.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; } - // Ensure connections to the relays - gossip.ensure_connections(&client, &urls).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( - &signer, - &receiver, - rumor.clone(), - vec![Tag::public_key(member)], - ) - .await?; + let event = + EventBuilder::gift_wrap(&signer, &receiver, rumor.clone(), vec![]).await?; // Send the gift wrap event to the messaging relays - match client.send_event_to(urls, &event).await { + match client.send_event_to(relays, &event).await { Ok(output) => { let id = output.id().to_owned(); let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); let report = SendReport::new(receiver).status(output); + let tracker = tracker().read().await; 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 - if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() { - let output = SendReport::new(receiver).status(output); - reports.push(output); + if tracker.is_sent_by_coop(&id) { + let output = Output::new(id); + let report = SendReport::new(receiver).status(output); + reports.push(report); break; } @@ -562,55 +510,35 @@ impl Room { } } - // 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); - } - - // 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( - &signer, - &receiver, - rumor.clone(), - vec![Tag::public_key(user_pubkey)], - ) - .await?; + let event = + EventBuilder::gift_wrap(&signer, ¤t_user, rumor.clone(), vec![]).await?; // Only send a backup message to current user if sent successfully to others 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() { - reports.push(SendReport::new(user_pubkey).relays_not_found()); + if current_user_relays.is_empty() { + reports.push(SendReport::new(current_user).relays_not_found()); return Ok(reports); } - // Ensure connections to the relays - gossip.ensure_connections(&client, &urls).await; + // Ensure relay connection + for url in current_user_relays.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } // Send the event to the messaging relays - match client.send_event_to(urls, &event).await { + match client.send_event_to(current_user_relays, &event).await { Ok(output) => { - reports.push(SendReport::new(user_pubkey).status(output)); + reports.push(SendReport::new(current_user).status(output)); } Err(e) => { - reports.push(SendReport::new(user_pubkey).error(e.to_string())); + reports.push(SendReport::new(current_user).error(e.to_string())); } } } else { - reports.push(SendReport::new(user_pubkey).on_hold(event)); + reports.push(SendReport::new(current_user).on_hold(event)); } Ok(reports) @@ -625,10 +553,8 @@ impl Room { ) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let gossip = nostr.read(cx).gossip(); cx.background_spawn(async move { - let gossip = gossip.read().await; let mut resend_reports = vec![]; for report in reports.into_iter() { @@ -657,23 +583,13 @@ impl Room { // Process the on hold event if it exists if let Some(event) = report.on_hold { - 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) => { - resend_reports.push(SendReport::new(receiver).status(output)); - } - Err(e) => { - resend_reports.push(SendReport::new(receiver).error(e.to_string())); - } + // Send the event to the messaging relays + match client.send_event(&event).await { + Ok(output) => { + resend_reports.push(SendReport::new(receiver).status(output)); + } + Err(e) => { + resend_reports.push(SendReport::new(receiver).error(e.to_string())); } } } @@ -682,31 +598,4 @@ impl Room { Ok(resend_reports) }) } - - fn select_signer(kind: &SignerKind, user: T, encryption: Option) -> Result - where - T: NostrSigner, - { - match kind { - SignerKind::Encryption => { - Ok(encryption.ok_or_else(|| anyhow!("No encryption key found"))?) - } - SignerKind::User => Ok(user), - SignerKind::Auto => Ok(encryption.unwrap_or(user)), - } - } - - fn select_receiver( - kind: &SignerKind, - member: PublicKey, - encryption: Option, - ) -> Result { - match kind { - SignerKind::Encryption => { - Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?) - } - SignerKind::User => Ok(member), - SignerKind::Auto => Ok(encryption.unwrap_or(member)), - } - } } diff --git a/crates/chat_ui/Cargo.toml b/crates/chat_ui/Cargo.toml index 9119b98..bff3e8d 100644 --- a/crates/chat_ui/Cargo.toml +++ b/crates/chat_ui/Cargo.toml @@ -9,8 +9,6 @@ state = { path = "../state" } ui = { path = "../ui" } theme = { path = "../theme" } common = { path = "../common" } -account = { path = "../account" } -encryption = { path = "../encryption" } person = { path = "../person" } chat = { path = "../chat" } settings = { path = "../settings" } diff --git a/crates/chat_ui/src/actions.rs b/crates/chat_ui/src/actions.rs index 878bc9c..bea282e 100644 --- a/crates/chat_ui/src/actions.rs +++ b/crates/chat_ui/src/actions.rs @@ -1,4 +1,3 @@ -use encryption::SignerKind; use gpui::Action; use nostr_sdk::prelude::*; use serde::Deserialize; @@ -7,10 +6,6 @@ use serde::Deserialize; #[action(namespace = chat, no_json)] pub struct SeenOn(pub EventId); -#[derive(Action, Clone, PartialEq, Eq, Deserialize)] -#[action(namespace = chat, no_json)] -pub struct SetSigner(pub SignerKind); - /// Define a open public key action #[derive(Action, Clone, PartialEq, Eq, Deserialize, Debug)] #[action(namespace = pubkey, no_json)] diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index bd4ccd5..aef827d 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -4,7 +4,6 @@ use std::time::Duration; pub use actions::*; use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendOptions, SendReport}; use common::{nip96_upload, RenderedProfile, RenderedTimestamp}; -use encryption::SignerKind; use gpui::prelude::FluentBuilder; use gpui::{ div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext, diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index 5e12fb5..391752b 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -2,12 +2,11 @@ pub const CLIENT_NAME: &str = "Coop"; pub const APP_ID: &str = "su.reya.coop"; /// Bootstrap Relays. -pub const BOOTSTRAP_RELAYS: [&str; 5] = [ +pub const BOOTSTRAP_RELAYS: [&str; 4] = [ "wss://relay.damus.io", "wss://relay.primal.net", "wss://relay.nos.social", "wss://user.kindpag.es", - "wss://purplepag.es", ]; /// Search Relays. diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index d6d8d75..df8e9ba 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -38,9 +38,6 @@ chat = { path = "../chat" } chat_ui = { path = "../chat_ui" } settings = { path = "../settings" } auto_update = { path = "../auto_update" } -account = { path = "../account" } -encryption = { path = "../encryption" } -encryption_ui = { path = "../encryption_ui" } person = { path = "../person" } relay_auth = { path = "../relay_auth" } diff --git a/crates/encryption/src/lib.rs b/crates/encryption/src/lib.rs deleted file mode 100644 index 857f863..0000000 --- a/crates/encryption/src/lib.rs +++ /dev/null @@ -1,682 +0,0 @@ -use std::collections::HashSet; -use std::sync::Arc; -use std::time::Duration; - -use account::Account; -use anyhow::{anyhow, Context as AnyhowContext, Error}; -use common::app_name; -use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; -use nostr_sdk::prelude::*; -pub use signer::*; -use smallvec::{smallvec, SmallVec}; -use state::{Announcement, NostrRegistry}; - -mod signer; - -pub fn init(cx: &mut App) { - Encryption::set_global(cx.new(Encryption::new), cx); -} - -struct GlobalEncryption(Entity); - -impl Global for GlobalEncryption {} - -pub struct Encryption { - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - /// - /// Client Signer that used for communication between devices - client_signer: Entity>>, - - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - /// - /// Encryption Key used for encryption and decryption instead of the user's identity - pub encryption: Entity>>, - - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - /// - /// Encryption Key announcement - announcement: Option>, - - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - /// - /// Requests for encryption keys from other devices - requests: Entity>, - - /// Async task for handling notifications - handle_notifications: Option>, - - /// Async task for handling requests - handle_requests: Option>, - - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 2]>, - - /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 1]>, -} - -impl Encryption { - /// Retrieve the global encryption state - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - /// Set the global encryption instance - fn set_global(state: Entity, cx: &mut App) { - cx.set_global(GlobalEncryption(state)); - } - - /// Create a new encryption instance - fn new(cx: &mut Context) -> Self { - let account = Account::global(cx); - - let requests = cx.new(|_| HashSet::default()); - let encryption = cx.new(|_| None); - let client_signer = cx.new(|_| None); - - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Observe the account state - cx.observe(&account, |this, state, 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); - } - }), - ); - - 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)); - } - }), - ); - - Self { - requests, - client_signer, - encryption, - announcement: None, - handle_notifications: None, - handle_requests: None, - _subscriptions: subscriptions, - _tasks: smallvec![], - } - } - - /// Encrypt and store a key in the local database. - async fn set_keys(client: &Client, kind: T, value: String) -> Result<(), Error> - where - T: Into, - { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - // Encrypt the value - let content = signer.nip44_encrypt(&public_key, value.as_ref()).await?; - - // Construct the application data event - let event = EventBuilder::new(Kind::ApplicationSpecificData, content) - .tag(Tag::identifier(format!("coop:{}", kind.into()))) - .build(public_key) - .sign(&Keys::generate()) - .await?; - - // Save the event to the database - client.database().save_event(&event).await?; - - Ok(()) - } - - /// Get and decrypt a key from the local database. - async fn get_keys(client: &Client, kind: T) -> Result - where - T: Into, - { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let filter = Filter::new() - .kind(Kind::ApplicationSpecificData) - .identifier(format!("coop:{}", kind.into())); - - if let Some(event) = client.database().query(filter).await?.first() { - let content = signer.nip44_decrypt(&public_key, &event.content).await?; - let secret = SecretKey::parse(&content)?; - let keys = Keys::new(secret); - - Ok(keys) - } else { - Err(anyhow!("Key not found")) - } - } - - /// 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(5); - - self._tasks.push( - // Run task in the background - cx.spawn(async move |this, cx| { - cx.background_executor().timer(delay).await; - - if let Ok(announcement) = task.await { - this.update(cx, |this, cx| { - this.load_encryption(&announcement, cx); - // Set the announcement - this.announcement = Some(Arc::new(announcement)); - cx.notify(); - }) - .expect("Entity has been released"); - } - }), - ); - } - - fn _get_announcement(&self, cx: &App) -> Task> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - cx.background_spawn(async move { - let user_signer = client.signer().await?; - let public_key = user_signer.get_public_key().await?; - - let filter = Filter::new() - .kind(Kind::Custom(10044)) - .author(public_key) - .limit(1); - - if let Some(event) = client.database().query(filter).await?.first() { - Ok(NostrRegistry::extract_announcement(event)?) - } else { - Err(anyhow!("Announcement not found")) - } - }) - } - - /// Load the encryption key that stored in the database - /// - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - fn load_encryption(&mut self, announcement: &Announcement, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let n = announcement.public_key(); - - cx.spawn(async move |this, cx| { - let result = Self::get_keys(&client, "encryption").await; - - this.update(cx, |this, cx| { - if let Ok(keys) = result { - if keys.public_key() == n { - this.set_encryption(Arc::new(keys), cx); - 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 - pub fn listen_request(&mut self, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let (tx, rx) = flume::bounded::(50); - - let task: Task> = cx.background_spawn({ - let client = nostr.read(cx).client(); - - async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let id = SubscriptionId::new("listen-request"); - - let filter = Filter::new() - .author(public_key) - .kind(Kind::Custom(4454)) - .since(Timestamp::now()); - - // Unsubscribe from the previous subscription - client.unsubscribe(&id).await; - - // Subscribe to the new subscription - client.subscribe_with_id(id, filter, None).await?; - - Ok(()) - } - }); - - // Run this task and finish in the background - task.detach(); - - // Handle notifications - self.handle_notifications = Some(cx.background_spawn(async move { - let mut notifications = client.notifications(); - let mut processed_events = HashSet::new(); - - while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, .. } = notification else { - // Skip if the notification is not a message - continue; - }; - - if let RelayMessage::Event { event, .. } = message { - if !processed_events.insert(event.id) { - // Skip if the event has already been processed - continue; - } - - if event.kind != Kind::Custom(4454) { - // Skip if the event is not a encryption events - continue; - }; - - if NostrRegistry::is_self_authored(&client, &event).await { - if let Ok(announcement) = NostrRegistry::extract_announcement(&event) { - tx.send_async(announcement).await.ok(); - } - } - } - } - })); - - // Handle requests - self.handle_requests = Some(cx.spawn(async move |this, cx| { - while let Ok(request) = rx.recv_async().await { - this.update(cx, |this, cx| { - this.set_request(request, cx); - }) - .expect("Entity has been released"); - } - })); - } - - /// Overwrite the encryption key - /// - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - 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(); - let secret = keys.secret_key().to_secret_hex(); - - // Create a task announce the encryption key - cx.background_spawn(async move { - // Store the encryption key to the database - 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.outbox_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), "") - .tags(vec![ - 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_to(write_relays, &event).await?; - - Ok(keys) - }) - } - - /// Send a request for encryption key from other clients - /// - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - 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 { - return Task::ready(Err(anyhow!("Client Signer is required"))); - }; - - cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let client_pubkey = client_signer.get_public_key().await?; - - // Get the encryption key approval response from the database first - let filter = Filter::new() - .kind(Kind::Custom(4455)) - .author(public_key) - .pubkey(client_pubkey) - .limit(1); - - match client.database().query(filter).await?.first_owned() { - Some(event) => { - let root_device = event - .tags - .find(TagKind::custom("P")) - .and_then(|tag| tag.content()) - .and_then(|content| PublicKey::parse(content).ok()) - .context("Invalid event's tags")?; - - let payload = event.content.as_str(); - let decrypted = client_signer.nip44_decrypt(&root_device, payload).await?; - - let secret = SecretKey::from_hex(&decrypted)?; - let keys = Keys::new(secret); - - Ok(Some(keys)) - } - None => { - let gossip = gossip.read().await; - let write_relays = gossip.outbox_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![ - Tag::client(app_name()), - Tag::custom(TagKind::custom("pubkey"), vec![client_pubkey]), - ]) - .sign(&signer) - .await?; - - // Send a request for encryption keys from other devices - client.send_event_to(&write_relays, &event).await?; - - // Create a unique ID to control the subscription later - let subscription_id = SubscriptionId::new("listen-response"); - - let filter = Filter::new() - .kind(Kind::Custom(4455)) - .author(public_key) - .since(Timestamp::now()); - - // Subscribe to the approval response event - client - .subscribe_with_id_to(&write_relays, subscription_id, filter, None) - .await?; - - Ok(None) - } - } - }) - } - - /// Send the approval response event - /// - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - 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 { - return Task::ready(Err(anyhow!("Client Signer is required"))); - }; - - 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.outbox_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?; - - // Encrypt the encryption keys with the client's signer - let payload = client_signer - .nip44_encrypt(&target, &encryption.secret_key().to_secret_hex()) - .await?; - - // Construct the response event - // - // P tag: the current client's public key - // p tag: the requester's public key - let event = EventBuilder::new(Kind::Custom(4455), payload) - .tags(vec![ - Tag::custom(TagKind::custom("P"), vec![client_pubkey]), - Tag::public_key(target), - ]) - .build(public_key) - .sign(&signer) - .await?; - - // Send the response event to the user's relay list - client.send_event_to(write_relays, &event).await?; - - Ok(()) - }) - } - - /// Wait for the approval response event - /// - /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md - pub fn wait_for_approval(&self, cx: &App) -> Task> { - 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 Task::ready(Err(anyhow!("Client Signer is required"))); - }; - - cx.background_spawn(async move { - let mut notifications = client.notifications(); - let mut processed_events = HashSet::new(); - - while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, .. } = notification else { - // Skip non-message notifications - continue; - }; - - if let RelayMessage::Event { event, .. } = message { - if !processed_events.insert(event.id) { - // Skip if the event has already been processed - continue; - } - - if event.kind != Kind::Custom(4455) { - // Skip non-response events - continue; - } - - if let Ok(response) = NostrRegistry::extract_response(&client, &event).await { - let public_key = response.public_key(); - let payload = response.payload(); - - // Decrypt the payload using the client signer - let decrypted = client_signer.nip44_decrypt(&public_key, payload).await?; - let secret = SecretKey::parse(&decrypted)?; - // Construct the encryption keys - let keys = Keys::new(secret); - - return Ok(keys); - } else { - log::error!("Failed to extract response from event"); - } - } - } - - Err(anyhow!("Failed to handle Encryption Key approval response")) - }) - } - - /// Set the client signer for the account - pub fn set_client(&mut self, signer: Arc, cx: &mut Context) { - self.client_signer.update(cx, |this, cx| { - *this = Some(signer); - cx.notify(); - }); - } - - /// Set the encryption signer for the account - pub fn set_encryption(&mut self, signer: Arc, cx: &mut Context) { - self.encryption.update(cx, |this, cx| { - *this = Some(signer); - cx.notify(); - }); - } - - /// Check if the account entity has an encryption key - pub fn has_encryption(&self, cx: &App) -> bool { - self.encryption.read(cx).is_some() - } - - /// Returns the encryption key - pub fn encryption_key(&self, cx: &App) -> Option> { - self.encryption.read(cx).clone() - } - - /// Returns the encryption announcement - pub fn announcement(&self) -> Option> { - self.announcement.clone() - } - - /// Returns the encryption requests - pub fn requests(&self) -> Entity> { - self.requests.clone() - } - - /// Push the encryption request - pub fn set_request(&mut self, request: Announcement, cx: &mut Context) { - self.requests.update(cx, |this, cx| { - this.insert(request); - 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/src/signer.rs b/crates/encryption/src/signer.rs deleted file mode 100644 index 59513ed..0000000 --- a/crates/encryption/src/signer.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Deserialize, Serialize)] -pub enum SignerKind { - Encryption, - #[default] - User, - Auto, -} diff --git a/crates/encryption_ui/Cargo.toml b/crates/encryption_ui/Cargo.toml deleted file mode 100644 index 62ba28f..0000000 --- a/crates/encryption_ui/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "encryption_ui" -version.workspace = true -edition.workspace = true -publish.workspace = true - -[dependencies] -state = { path = "../state" } -ui = { path = "../ui" } -theme = { path = "../theme" } -common = { path = "../common" } -account = { path = "../account" } -encryption = { path = "../encryption" } -person = { path = "../person" } -settings = { path = "../settings" } - -gpui.workspace = true - -nostr-sdk.workspace = true -anyhow.workspace = true -itertools.workspace = true -smallvec.workspace = true -smol.workspace = true -log.workspace = true -futures.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/crates/encryption_ui/src/lib.rs b/crates/encryption_ui/src/lib.rs deleted file mode 100644 index 289ef60..0000000 --- a/crates/encryption_ui/src/lib.rs +++ /dev/null @@ -1,464 +0,0 @@ -use std::cell::Cell; -use std::rc::Rc; -use std::sync::Arc; -use std::time::Duration; - -use anyhow::anyhow; -use common::shorten_pubkey; -use encryption::Encryption; -use futures::FutureExt; -use gpui::prelude::FluentBuilder; -use gpui::{ - div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, - Styled, Subscription, Window, -}; -use smallvec::{smallvec, SmallVec}; -use state::Announcement; -use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::notification::Notification; -use ui::{h_flex, v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| EncryptionPanel::new(window, cx)) -} - -#[derive(Debug)] -pub struct EncryptionPanel { - /// Whether the panel is currently requesting encryption. - requesting: bool, - - /// Whether the panel is currently creating encryption. - creating: bool, - - /// Whether the panel is currently showing an error. - error: Entity>, - - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, -} - -impl EncryptionPanel { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let error = cx.new(|_| None); - - let encryption = Encryption::global(cx); - let requests = encryption.read(cx).requests(); - - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Observe encryption request - cx.observe_in(&requests, window, |this, state, window, cx| { - for req in state.read(cx).clone().into_iter() { - this.ask_for_approval(req, window, cx); - } - }), - ); - - Self { - requesting: false, - creating: false, - error, - _subscriptions: subscriptions, - } - } - - fn set_requesting(&mut self, status: bool, cx: &mut Context) { - self.requesting = status; - cx.notify(); - } - - fn set_creating(&mut self, status: bool, cx: &mut Context) { - self.creating = status; - cx.notify(); - } - - fn set_error(&mut self, error: impl Into, cx: &mut Context) { - self.error.update(cx, |this, cx| { - *this = Some(error.into()); - cx.notify(); - }); - } - - fn request(&mut self, window: &mut Window, cx: &mut Context) { - let encryption = Encryption::global(cx); - let send_request = encryption.read(cx).send_request(cx); - - // Ensure the user has not sent multiple requests - if self.requesting { - return; - } - self.set_requesting(true, cx); - - cx.spawn_in(window, async move |this, cx| { - match send_request.await { - Ok(Some(keys)) => { - this.update(cx, |this, cx| { - this.set_requesting(false, cx); - // Set the encryption key - encryption.update(cx, |this, cx| { - this.set_encryption(Arc::new(keys), cx); - }); - }) - .expect("Entity has been released"); - } - Ok(None) => { - this.update_in(cx, |this, window, cx| { - this.wait_for_approval(window, cx); - }) - .expect("Entity has been released"); - } - Err(e) => { - this.update(cx, |this, cx| { - this.set_requesting(false, cx); - this.set_error(e.to_string(), cx); - }) - .expect("Entity has been released"); - } - } - }) - .detach(); - } - - fn new_encryption(&mut self, window: &mut Window, cx: &mut Context) { - let encryption = Encryption::global(cx); - let reset = encryption.read(cx).new_encryption(cx); - - // Ensure the user has not sent multiple requests - if self.requesting { - return; - } - self.set_creating(true, cx); - - cx.spawn_in(window, async move |this, cx| { - match reset.await { - Ok(keys) => { - this.update(cx, |this, cx| { - this.set_creating(false, cx); - // Set the encryption key - encryption.update(cx, |this, cx| { - this.set_encryption(Arc::new(keys), cx); - this.listen_request(cx); - }); - }) - .expect("Entity has been released"); - } - Err(e) => { - this.update(cx, |this, cx| { - this.set_creating(false, cx); - this.set_error(e.to_string(), cx); - }) - .expect("Entity has been released"); - } - } - }) - .detach(); - } - - fn wait_for_approval(&mut self, window: &mut Window, cx: &mut Context) { - let encryption = Encryption::global(cx); - let wait_for_approval = encryption.read(cx).wait_for_approval(cx); - - cx.spawn_in(window, async move |this, cx| { - let timeout = cx.background_executor().timer(Duration::from_secs(30)); - - let result = futures::select! { - result = wait_for_approval.fuse() => { - // Ok(keys) - result - }, - _ = timeout.fuse() => { - Err(anyhow!("Timeout")) - } - }; - - this.update(cx, |this, cx| { - match result { - Ok(keys) => { - this.set_requesting(false, cx); - // Set the encryption key - encryption.update(cx, |this, cx| { - this.set_encryption(Arc::new(keys), cx); - }); - } - Err(e) => { - this.set_requesting(false, cx); - this.set_error(e.to_string(), cx); - } - }; - }) - .expect("Entity has been released"); - }) - .detach(); - } - - fn ask_for_approval(&mut self, req: Announcement, window: &mut Window, cx: &mut Context) { - let client_name = req.client_name(); - 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(id.clone()) - .autohide(false) - .icon(IconName::Encryption) - .title(SharedString::from("Encryption Key Request")) - .content(move |_window, cx| { - v_flex() - .gap_2() - .text_sm() - .child(SharedString::from( - "You've requested for the Encryption Key from:", - )) - .child( - v_flex() - .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| { - Button::new("approve") - .label("Approve") - .small() - .primary() - .loading(loading.get()) - .disabled(loading.get()) - .on_click({ - let loading = Rc::clone(&loading); - let id = id.clone(); - - 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(); - } - }) - }); - - // Push the notification to the current window - window.push_notification(note, cx); - - // Focus the window if it's not active - if !window.is_window_hovered() { - window.activate_window(); - } - } -} - -impl Render for EncryptionPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const NOTICE: &str = "Found an Encryption Announcement"; - const SUGGEST: &str = "Please request the Encryption Key to continue using."; - - const DESCRIPTION: &str = "Encryption Key is used to replace the User's Identity in encryption and decryption messages. Coop will automatically fallback to User's Identity if needed."; - 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() - .p_2() - .max_w(px(340.)) - .w(px(340.)) - .text_sm() - .when_some(announcement.as_ref(), |this, announcement| { - let pubkey = shorten_pubkey(announcement.public_key(), 16); - let client_name = announcement.client_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")), - ) - }) - .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(client_name.clone()), - ), - ) - .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_2() - .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_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/relay_auth/Cargo.toml b/crates/relay_auth/Cargo.toml index 1d96c01..1b88afd 100644 --- a/crates/relay_auth/Cargo.toml +++ b/crates/relay_auth/Cargo.toml @@ -17,4 +17,5 @@ nostr-sdk.workspace = true anyhow.workspace = true smallvec.workspace = true smol.workspace = true +flume.workspace = true log.workspace = true diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 221bb2b..e63f361 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::cell::Cell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::hash::{Hash, Hasher}; use std::rc::Rc; @@ -12,7 +12,7 @@ use gpui::{ use nostr_sdk::prelude::*; use settings::AppSettings; use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; +use state::{tracker, NostrRegistry}; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::notification::Notification; @@ -25,10 +25,7 @@ pub fn init(window: &mut Window, cx: &mut App) { RelayAuth::set_global(cx.new(|cx| RelayAuth::new(window, cx)), cx); } -struct GlobalRelayAuth(Entity); - -impl Global for GlobalRelayAuth {} - +/// Authentication request #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct AuthRequest { pub url: RelayUrl, @@ -50,6 +47,11 @@ impl AuthRequest { } } +struct GlobalRelayAuth(Entity); + +impl Global for GlobalRelayAuth {} + +// Relay authentication #[derive(Debug)] pub struct RelayAuth { /// Entity for managing auth requests @@ -77,8 +79,13 @@ impl RelayAuth { fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + + // Get the current entity let entity = cx.entity(); + // Channel for communication between nostr and gpui + let (tx, rx) = flume::bounded::(100); + let mut subscriptions = smallvec![]; let mut tasks = smallvec![]; @@ -103,26 +110,20 @@ impl RelayAuth { ); tasks.push( - // Handle notifications + // Handle nostr notifications + cx.background_spawn(async move { + Self::handle_notifications(&client, &tx).await; + }), + ); + + tasks.push( + // Update GPUI states cx.spawn(async move |this, cx| { - let mut notifications = client.notifications(); - let mut challenges: HashSet> = HashSet::new(); - - while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, relay_url } = notification else { - // Skip if the notification is not a message - continue; - }; - - if let RelayMessage::Auth { challenge } = message { - if challenges.insert(challenge.clone()) { - this.update(cx, |this, cx| { - this.requests.insert(AuthRequest::new(challenge, relay_url)); - cx.notify(); - }) - .expect("Entity has been released"); - }; - } + while let Ok(request) = rx.recv_async().await { + this.update(cx, |this, cx| { + this.add_request(request, cx); + }) + .ok(); } }), ); @@ -134,6 +135,31 @@ impl RelayAuth { } } + // Handle nostr notifications + async fn handle_notifications(client: &Client, tx: &flume::Sender) { + let mut notifications = client.notifications(); + let mut processed_challenges = HashSet::new(); + + while let Ok(notification) = notifications.recv().await { + if let RelayPoolNotification::Message { + message: RelayMessage::Auth { challenge }, + relay_url, + } = notification + { + if processed_challenges.insert(challenge.clone()) { + let request = AuthRequest::new(challenge, relay_url); + tx.send_async(request).await.ok(); + }; + } + } + } + + /// Add a new authentication request. + fn add_request(&mut self, request: AuthRequest, cx: &mut Context) { + self.requests.insert(request); + cx.notify(); + } + /// Get the number of pending requests. pub fn pending_requests(&self, _cx: &App) -> usize { self.requests.len() @@ -152,7 +178,6 @@ impl RelayAuth { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let tracker = nostr.read(cx).tracker(); let challenge = req.challenge.to_owned(); let url = req.url.to_owned(); @@ -190,30 +215,14 @@ impl RelayAuth { // Re-subscribe to previous subscription relay.resubscribe().await?; - // Get all failed events that need to be resent - let mut tracker = tracker.write().await; - - let ids: Vec = tracker - .resend_queue - .iter() - .filter(|(_, url)| relay_url == *url) - .map(|(id, _)| *id) - .collect(); + // Get all pending events that need to be resent + let mut tracker = tracker().write().await; + let ids: Vec = tracker.pending_resend(relay_url); for id in ids.into_iter() { - if let Some(relay_url) = tracker.resend_queue.remove(&id) { - if let Some(event) = client.database().event_by_id(&id).await? { - let event_id = relay.send_event(&event).await?; - - let output = Output { - val: event_id, - failed: HashMap::new(), - success: HashSet::from([relay_url]), - }; - - tracker.sent_ids.insert(event_id); - tracker.resent_ids.push(output); - } + if let Some(event) = client.database().event_by_id(&id).await? { + let event_id = relay.send_event(&event).await?; + tracker.sent(event_id); } } diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 9abb248..51ac44e 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -12,10 +12,8 @@ nostr-lmdb.workspace = true gpui.workspace = true smol.workspace = true -smallvec.workspace = true +flume.workspace = true log.workspace = true anyhow.workspace = true -serde.workspace = true -serde_json.workspace = true -rustls = "0.23.23" +rustls = "0.23" diff --git a/crates/state/src/event.rs b/crates/state/src/event.rs new file mode 100644 index 0000000..e7de936 --- /dev/null +++ b/crates/state/src/event.rs @@ -0,0 +1,46 @@ +use std::collections::HashSet; +use std::sync::{Arc, OnceLock}; + +use nostr_sdk::prelude::*; +use smol::lock::RwLock; + +static TRACKER: OnceLock>> = OnceLock::new(); + +pub fn tracker() -> &'static Arc> { + TRACKER.get_or_init(|| Arc::new(RwLock::new(EventTracker::default()))) +} + +/// Event tracker +#[derive(Debug, Clone, Default)] +pub struct EventTracker { + /// Tracking events sent by Coop in the current session + sent_ids: HashSet, + + /// Events that need to be resent later + pending_resend: HashSet<(EventId, RelayUrl)>, +} + +impl EventTracker { + /// Check if an event was sent by Coop in the current session. + pub fn is_sent_by_coop(&self, id: &EventId) -> bool { + self.sent_ids.contains(id) + } + + /// Mark an event as sent by Coop. + pub fn sent(&mut self, id: EventId) { + self.sent_ids.insert(id); + } + + /// Get all events that need to be resent later for a specific relay. + pub fn pending_resend(&mut self, relay: &RelayUrl) -> Vec { + self.pending_resend + .extract_if(|(_id, url)| url == relay) + .map(|(id, _url)| id) + .collect() + } + + /// Add an event (id and relay url) to the pending resend set. + pub fn add_to_pending(&mut self, id: EventId, url: RelayUrl) { + self.pending_resend.insert((id, url)); + } +} diff --git a/crates/state/src/gossip.rs b/crates/state/src/gossip.rs new file mode 100644 index 0000000..48440aa --- /dev/null +++ b/crates/state/src/gossip.rs @@ -0,0 +1,103 @@ +use std::collections::{HashMap, HashSet}; + +use nostr_sdk::prelude::*; + +/// Gossip +#[derive(Debug, Clone, Default)] +pub struct Gossip { + /// Gossip relays for each public key + relays: HashMap)>>, + /// Messaging relays for each public key + messaging_relays: HashMap>, +} + +impl Gossip { + /// Get read relays for a given public key + pub fn read_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() + } + + /// Get write relays for a given public key + pub fn write_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() + } + + /// 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), + ); + } + + /// Get messaging relays for a given 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), + ); + } +} diff --git a/crates/state/src/identity.rs b/crates/state/src/identity.rs new file mode 100644 index 0000000..2d296b4 --- /dev/null +++ b/crates/state/src/identity.rs @@ -0,0 +1,83 @@ +use nostr_sdk::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RelayState { + #[default] + Initial, + NotSet, + Set, +} + +impl RelayState { + pub fn is_initial(&self) -> bool { + matches!(self, RelayState::Initial) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct Identity { + /// The public key of the account + public_key: Option, + + /// Status of the current user NIP-65 relays + relay_list: RelayState, + + /// Status of the current user NIP-17 relays + messaging_relays: RelayState, +} + +impl AsRef for Identity { + fn as_ref(&self) -> &Identity { + self + } +} + +impl Identity { + pub fn new() -> Self { + Self { + public_key: None, + relay_list: RelayState::default(), + messaging_relays: RelayState::default(), + } + } + + /// Sets the state of the NIP-65 relays. + pub fn set_relay_list_state(&mut self, state: RelayState) { + self.relay_list = state; + } + + /// Returns the state of the NIP-65 relays. + pub fn relay_list_state(&self) -> RelayState { + self.relay_list + } + + pub fn set_messaging_relays_state(&mut self, state: RelayState) { + self.messaging_relays = state; + } + + /// Returns the state of the NIP-17 relays. + pub fn messaging_relays_state(&self) -> RelayState { + self.messaging_relays + } + + /// Returns true if the identity has a public key. + pub fn has_public_key(&self) -> bool { + self.public_key.is_some() + } + + /// Sets the public key of the identity. + pub fn set_public_key(&mut self, public_key: PublicKey) { + self.public_key = Some(public_key); + } + + /// Unsets the public key of the identity. + pub fn unset_public_key(&mut self) { + self.public_key = None; + } + + /// Returns the public key of the identity. + pub fn public_key(&self) -> PublicKey { + // This method is safe to unwrap because the public key is always called when the identity is created. + self.public_key.unwrap() + } +} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index de10ecd..67201b8 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,26 +1,32 @@ use std::collections::HashSet; -use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Context as AnyhowContext, Error}; +use anyhow::Error; use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; -use gpui::{App, AppContext, Context, Entity, Global, Task}; +use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; -use smallvec::{smallvec, SmallVec}; -use smol::lock::RwLock; -pub use storage::*; -pub use tracker::*; -mod storage; -mod tracker; +mod event; +mod gossip; +mod identity; -pub const GIFTWRAP_SUBSCRIPTION: &str = "gift-wrap-events"; +pub use event::*; +pub use gossip::*; +pub use identity::*; + +use crate::identity::Identity; pub fn init(cx: &mut App) { NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); } +/// Default timeout for subscription +pub const TIMEOUT: u64 = 3; + +/// Default subscription id for gift wrap events +pub const GIFTWRAP_SUBSCRIPTION: &str = "giftwrap-events"; + struct GlobalNostrRegistry(Entity); impl Global for GlobalNostrRegistry {} @@ -28,17 +34,27 @@ impl Global for GlobalNostrRegistry {} /// Nostr Registry #[derive(Debug)] pub struct NostrRegistry { - /// Nostr Client + /// Nostr client client: Client, - /// Custom gossip implementation - gossip: Arc>, + /// App keys + /// + /// Used for Nostr Connect and NIP-4e operations + app_keys: Keys, - /// Tracks activity related to Nostr events - tracker: Arc>, + /// Current identity (user's public key) + /// + /// Set by the current Nostr signer + identity: Entity, + + /// Gossip implementation + gossip: Entity, /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 1]>, + tasks: Vec>>, + + /// Subscriptions + _subscriptions: Vec, } impl NostrRegistry { @@ -79,310 +95,424 @@ impl NostrRegistry { // Construct the nostr client let client = ClientBuilder::default().database(lmdb).opts(opts).build(); + let _ = tracker(); - let tracker = Arc::new(RwLock::new(EventTracker::default())); - let gossip = Arc::new(RwLock::new(Gossip::default())); + // Get the app keys + let app_keys = Self::create_or_init_app_keys().unwrap(); - let mut tasks = smallvec![]; + // Construct the gossip entity + let gossip = cx.new(|_| Gossip::default()); + let async_gossip = gossip.downgrade(); + + // Construct the identity entity + let identity = cx.new(|_| Identity::default()); + + // Channel for communication between nostr and gpui + let (tx, rx) = flume::bounded::(2048); + + let mut subscriptions = vec![]; + let mut tasks = vec![]; + + subscriptions.push( + // Observe the identity entity + cx.observe(&identity, |this, state, cx| { + let identity = state.read(cx); + + if identity.has_public_key() { + match identity.relay_list_state() { + RelayState::Initial => { + this.get_relay_list(cx); + } + RelayState::Set => match identity.messaging_relays_state() { + RelayState::Initial => { + this.get_messaging_relays(cx); + } + RelayState::Set => { + this.get_messages(cx); + } + _ => {} + }, + _ => {} + } + } + }), + ); tasks.push( // Establish connection to the bootstrap relays - // - // And handle notifications from the nostr relay pool channel cx.background_spawn({ let client = client.clone(); - let gossip = Arc::clone(&gossip); - let tracker = Arc::clone(&tracker); - let _ = initialized_at(); async move { - // Connect to the bootstrap relays - Self::connect(&client).await; + // Add bootstrap relay to the relay pool + for url in BOOTSTRAP_RELAYS.into_iter() { + client.add_relay(url).await?; + } - // Handle notifications from the relay pool - Self::handle_notifications(&client, &gossip, &tracker).await; + // Add search relay to the relay pool + for url in SEARCH_RELAYS.into_iter() { + client.add_relay(url).await?; + } + + // Connect to all added relays + client.connect().await; + + Ok(()) } }), ); + tasks.push( + // Handle nostr notifications + cx.background_spawn({ + let client = client.clone(); + + async move { Self::handle_notifications(&client, &tx).await } + }), + ); + + tasks.push( + // Update GPUI states + cx.spawn(async move |_this, cx| { + while let Ok(event) = rx.recv_async().await { + match event.kind { + Kind::RelayList => { + async_gossip.update(cx, |this, cx| { + this.insert_relays(&event); + cx.notify(); + })?; + } + Kind::InboxRelays => { + async_gossip.update(cx, |this, cx| { + this.insert_messaging_relays(&event); + cx.notify(); + })?; + } + _ => {} + } + } + + Ok(()) + }), + ); + Self { client, - tracker, + identity, gossip, - _tasks: tasks, + app_keys, + _subscriptions: subscriptions, + tasks, } } - /// Establish connection to the bootstrap relays - async fn connect(client: &Client) { - // Get all bootstrapping relays - let mut urls = vec![]; - urls.extend(BOOTSTRAP_RELAYS); - urls.extend(SEARCH_RELAYS); - - // Add relay to the relay pool - for url in urls.into_iter() { - client.add_relay(url).await.ok(); - } - - // Connect to all added relays - client.connect().await; - } - - async fn handle_notifications( - client: &Client, - gossip: &Arc>, - tracker: &Arc>, - ) { + // Handle nostr notifications + async fn handle_notifications(client: &Client, tx: &flume::Sender) -> Result<(), Error> { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); while let Ok(notification) = notifications.recv().await { - let RelayPoolNotification::Message { message, relay_url } = notification else { - // Skip if the notification is not a message - continue; - }; + if let RelayPoolNotification::Message { message, relay_url } = notification { + match message { + RelayMessage::Event { event, .. } => { + if !processed_events.insert(event.id) { + // Skip if the event has already been processed + continue; + } - match message { - RelayMessage::Event { event, .. } => { - if !processed_events.insert(event.id) { - // Skip if the event has already been processed - continue; + match event.kind { + Kind::RelayList => { + tx.send_async(event.into_owned()).await?; + } + Kind::InboxRelays => { + tx.send_async(event.into_owned()).await?; + } + _ => {} + } } + RelayMessage::Ok { + event_id, message, .. + } => { + let msg = MachineReadablePrefix::parse(&message); + let mut tracker = tracker().write().await; - match event.kind { - Kind::RelayList => { - let mut gossip = gossip.write().await; - gossip.insert_relays(&event); - - let urls: Vec = Self::extract_write_relays(&event); - let author = event.pubkey; - - log::info!("Write relays: {urls:?}"); - - // 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::get(client, &urls, author, Kind::Metadata).await; - // Fetch user's contact list event - Self::get(client, &urls, author, Kind::ContactList).await; - } + // Handle authentication messages + if let Some(MachineReadablePrefix::AuthRequired) = msg { + // Keep track of events that need to be resent after authentication + tracker.add_to_pending(event_id, relay_url); + } else { + // Keep track of events sent by Coop + tracker.sent(event_id) } - Kind::InboxRelays => { - let mut gossip = gossip.write().await; - gossip.insert_messaging_relays(&event); - - if Self::is_self_authored(client, &event).await { - // Extract user's messaging relays - let urls: Vec = - nip17::extract_relay_list(&event).cloned().collect(); - - // Fetch user's inbox messages in the extracted relays - Self::get_messages(client, event.pubkey, &urls).await; - } - } - Kind::Custom(10044) => { - let mut gossip = gossip.write().await; - gossip.insert_announcement(&event); - } - Kind::ContactList => { - if Self::is_self_authored(client, &event).await { - let public_keys: Vec = - event.tags.public_keys().copied().collect(); - - if let Err(e) = - Self::get_metadata_for_list(client, public_keys).await - { - log::error!("Failed to get metadata for list: {e}"); - } - } - } - _ => {} - }; - } - RelayMessage::Ok { - event_id, message, .. - } => { - let msg = MachineReadablePrefix::parse(&message); - let mut tracker = tracker.write().await; - - // Message that need to be authenticated will be handled separately - if let Some(MachineReadablePrefix::AuthRequired) = msg { - // Keep track of events that need to be resent after authentication - tracker.resend_queue.insert(event_id, relay_url); - } else { - // Keep track of events sent by Coop - tracker.sent_ids.insert(event_id); } + _ => {} } - _ => {} } } - } - - /// Check if event is published by current user - pub async fn is_self_authored(client: &Client, event: &Event) -> bool { - if let Ok(signer) = client.signer().await { - if let Ok(public_key) = signer.get_public_key().await { - return public_key == event.pubkey; - } - } - false - } - - /// 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 - 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 - 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); - - // Unsubscribe from the previous subscription - client.unsubscribe(&id).await; - - // Subscribe to filters to user's messaging relays - 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]; - - // Return if the list is empty - if pubkeys.is_empty() { - return Err(anyhow!("You need at least one public key".to_string(),)); - } - - let filter = Filter::new() - .limit(pubkeys.len() * kinds.len()) - .authors(pubkeys) - .kinds(kinds); - - // Subscribe to filters to the bootstrap relays - client - .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) - .await?; Ok(()) } - pub fn extract_read_relays(event: &Event) -> Vec { - nip65::extract_relay_list(event) - .filter_map(|(url, metadata)| { - if metadata.is_none() || metadata == &Some(RelayMetadata::Read) { - Some(url.to_owned()) - } else { - None - } - }) - .take(3) - .collect() + /// Get or create a new app keys + fn create_or_init_app_keys() -> Result { + let dir = config_dir().join(".app_keys"); + let content = match std::fs::read(&dir) { + Ok(content) => content, + Err(_) => { + // Generate new keys if file doesn't exist + let keys = Keys::generate(); + let secret_key = keys.secret_key(); + + std::fs::create_dir_all(dir.parent().unwrap())?; + std::fs::write(&dir, secret_key.to_secret_bytes())?; + + return Ok(keys); + } + }; + let secret_key = SecretKey::from_slice(&content)?; + let keys = Keys::new(secret_key); + + Ok(keys) } - pub 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 - .tags - .iter() - .find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey") - .and_then(|tag| tag.content()) - .and_then(|c| PublicKey::parse(c).ok()) - .context("Cannot parse public key from the event's tags")?; - - let client_name = event - .tags - .find(TagKind::Client) - .and_then(|tag| tag.content()) - .map(|c| c.to_string()); - - Ok(Announcement::new(event.id, client_name, public_key)) - } - - /// Extract an encryption keys response from an event. - pub async fn extract_response(client: &Client, event: &Event) -> Result { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - if event.pubkey != public_key { - return Err(anyhow!("Event does not belong to current user")); - } - - let client_pubkey = event - .tags - .find(TagKind::custom("P")) - .and_then(|tag| tag.content()) - .and_then(|c| PublicKey::parse(c).ok()) - .context("Cannot parse public key from the event's tags")?; - - Ok(Response::new(event.content.clone(), client_pubkey)) - } - - /// Returns a reference to the nostr client. + /// Get the nostr client pub fn client(&self) -> Client { self.client.clone() } - /// Returns a reference to the event tracker. - pub fn tracker(&self) -> Arc> { - Arc::clone(&self.tracker) + /// Get the app keys + pub fn app_keys(&self) -> &Keys { + &self.app_keys } - /// Returns a reference to the cache manager. - pub fn gossip(&self) -> Arc> { - Arc::clone(&self.gossip) + /// Get current identity + pub fn identity(&self, cx: &App) -> Identity { + self.identity.read(cx).clone() + } + + /// Get a relay hint (messaging relay) for a given public key + pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option { + self.gossip + .read(cx) + .messaging_relays(public_key) + .first() + .cloned() + } + + /// Get a list of messaging relays for a given public key + pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Vec { + self.gossip.read(cx).messaging_relays(public_key) + } + + /// Set the signer for the nostr client and verify the public key + pub fn set_signer(&mut self, signer: T, cx: &mut Context) + where + T: NostrSigner + 'static, + { + let client = self.client(); + let identity = self.identity.downgrade(); + + // Create a task to update the signer and verify the public key + let task: Task> = cx.background_spawn(async move { + // Update signer + client.set_signer(signer).await; + + // Verify signer + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + Ok(public_key) + }); + + self.tasks.push(cx.spawn(async move |_this, cx| { + match task.await { + Ok(public_key) => { + identity.update(cx, |this, cx| { + this.set_public_key(public_key); + cx.notify(); + })?; + } + Err(e) => { + log::error!("Failed to set signer: {e}"); + } + }; + + Ok(()) + })); + } + + /// Unset the current signer + pub fn unset_signer(&mut self, cx: &mut Context) { + let client = self.client(); + let async_identity = self.identity.downgrade(); + + self.tasks.push(cx.spawn(async move |_this, cx| { + // Unset the signer from nostr client + cx.background_executor() + .await_on_background(async move { + client.unset_signer().await; + }) + .await; + + // Unset the current identity + async_identity + .update(cx, |this, cx| { + this.unset_public_key(); + cx.notify(); + }) + .ok(); + + Ok(()) + })); + } + + // Get relay list for current user + fn get_relay_list(&mut self, cx: &mut Context) { + let client = self.client(); + let async_identity = self.identity.downgrade(); + let public_key = self.identity(cx).public_key(); + + let task: Task> = cx.background_spawn(async move { + let filter = Filter::new() + .kind(Kind::RelayList) + .author(public_key) + .limit(1); + + let mut stream = client + .stream_events_from(BOOTSTRAP_RELAYS, vec![filter], Duration::from_secs(TIMEOUT)) + .await?; + + while let Some((_url, res)) = stream.next().await { + if let Ok(event) = res { + log::info!("Received relay list event: {event:?}"); + return Ok(RelayState::Set); + } + } + + Ok(RelayState::NotSet) + }); + + self.tasks.push(cx.spawn(async move |_this, cx| { + match task.await { + Ok(state) => { + async_identity + .update(cx, |this, cx| { + this.set_relay_list_state(state); + cx.notify(); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to get relay list: {e}"); + } + } + + Ok(()) + })); + } + + /// Get messaging relays for current user + fn get_messaging_relays(&mut self, cx: &mut Context) { + let client = self.client(); + let async_identity = self.identity.downgrade(); + let public_key = self.identity(cx).public_key(); + let write_relays = self.gossip.read(cx).write_relays(&public_key); + + let task: Task> = cx.background_spawn(async move { + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + let mut stream = client + .stream_events_from(write_relays, vec![filter], Duration::from_secs(TIMEOUT)) + .await?; + + while let Some((_url, res)) = stream.next().await { + if let Ok(event) = res { + log::info!("Received messaging relays event: {event:?}"); + return Ok(RelayState::Set); + } + } + + Ok(RelayState::NotSet) + }); + + self.tasks.push(cx.spawn(async move |_this, cx| { + match task.await { + Ok(state) => { + async_identity + .update(cx, |this, cx| { + this.set_messaging_relays_state(state); + cx.notify(); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to get messaging relays: {e}"); + } + } + + Ok(()) + })); + } + + /// Continuously get gift wrap events for the current user in their messaging relays + fn get_messages(&mut self, cx: &mut Context) { + let client = self.client(); + let public_key = self.identity(cx).public_key(); + let messaging_relays = self.gossip.read(cx).messaging_relays(&public_key); + + cx.background_spawn(async move { + let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + + if let Err(e) = client + .subscribe_with_id_to(messaging_relays, id, vec![filter], None) + .await + { + log::error!("Failed to subscribe to gift wrap events: {e}"); + } + }) + .detach(); + } + + /// Publish an event to author's write relays + pub fn publish(&self, event: Event, cx: &App) -> Task, Error>> { + let client = self.client(); + let write_relays = self.gossip.read(cx).write_relays(&event.pubkey); + + cx.background_spawn(async move { Ok(client.send_event_to(&write_relays, &event).await?) }) + } + + /// Subscribe to event kinds to author's write relays + pub fn subscribe(&self, kinds: I, author: PublicKey, cx: &App) + where + I: Into>, + { + let client = self.client(); + let write_relays = self.gossip.read(cx).write_relays(&author); + + // Construct filters based on event kinds + let filters: Vec = kinds + .into() + .into_iter() + .map(|kind| Filter::new().kind(kind).author(author).limit(1)) + .collect(); + + // Construct subscription options + let opts = SubscribeAutoCloseOptions::default() + .timeout(Some(Duration::from_secs(TIMEOUT))) + .exit_policy(ReqExitPolicy::ExitOnEOSE); + + cx.background_spawn(async move { + if let Err(e) = client + .subscribe_to(&write_relays, filters, Some(opts)) + .await + { + log::error!("Failed to create a subscription: {e}"); + }; + }) + .detach(); } } diff --git a/crates/encryption/Cargo.toml b/crates/state_old/Cargo.toml similarity index 71% rename from crates/encryption/Cargo.toml rename to crates/state_old/Cargo.toml index 9c64c66..5f668cd 100644 --- a/crates/encryption/Cargo.toml +++ b/crates/state_old/Cargo.toml @@ -1,22 +1,21 @@ [package] -name = "encryption" +name = "state_old" version.workspace = true edition.workspace = true publish.workspace = true [dependencies] -state = { path = "../state" } common = { path = "../common" } -account = { path = "../account" } + +nostr-sdk.workspace = true +nostr-lmdb.workspace = true gpui.workspace = true -nostr-sdk.workspace = true - -anyhow.workspace = true -smallvec.workspace = true smol.workspace = true -futures.workspace = true -flume.workspace = true +smallvec.workspace = true log.workspace = true +anyhow.workspace = true serde.workspace = true serde_json.workspace = true + +rustls = "0.23.23" diff --git a/crates/state_old/src/lib.rs b/crates/state_old/src/lib.rs new file mode 100644 index 0000000..de10ecd --- /dev/null +++ b/crates/state_old/src/lib.rs @@ -0,0 +1,388 @@ +use std::collections::HashSet; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{anyhow, Context as AnyhowContext, Error}; +use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use gpui::{App, AppContext, Context, Entity, Global, Task}; +use nostr_lmdb::NostrLmdb; +use nostr_sdk::prelude::*; +use smallvec::{smallvec, SmallVec}; +use smol::lock::RwLock; +pub use storage::*; +pub use tracker::*; + +mod storage; +mod tracker; + +pub const GIFTWRAP_SUBSCRIPTION: &str = "gift-wrap-events"; + +pub fn init(cx: &mut App) { + NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); +} + +struct GlobalNostrRegistry(Entity); + +impl Global for GlobalNostrRegistry {} + +/// Nostr Registry +#[derive(Debug)] +pub struct NostrRegistry { + /// Nostr Client + client: Client, + + /// Custom gossip implementation + gossip: Arc>, + + /// Tracks activity related to Nostr events + tracker: Arc>, + + /// Tasks for asynchronous operations + _tasks: SmallVec<[Task<()>; 1]>, +} + +impl NostrRegistry { + /// Retrieve the global nostr state + pub fn global(cx: &App) -> Entity { + cx.global::().0.clone() + } + + /// Set the global nostr instance + fn set_global(state: Entity, cx: &mut App) { + cx.set_global(GlobalNostrRegistry(state)); + } + + /// Create a new nostr instance + fn new(cx: &mut Context) -> Self { + // rustls uses the `aws_lc_rs` provider by default + // This only errors if the default provider has already + // been installed. We can ignore this `Result`. + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .ok(); + + // Construct the nostr client options + let opts = ClientOptions::new() + .automatic_authentication(false) + .verify_subscriptions(false) + .sleep_when_idle(SleepWhenIdle::Enabled { + timeout: Duration::from_secs(600), + }); + + // Construct the lmdb + let lmdb = cx.background_executor().block(async move { + let path = config_dir().join("nostr"); + NostrLmdb::open(path) + .await + .expect("Failed to initialize database") + }); + + // Construct the nostr client + let client = ClientBuilder::default().database(lmdb).opts(opts).build(); + + let tracker = Arc::new(RwLock::new(EventTracker::default())); + let gossip = Arc::new(RwLock::new(Gossip::default())); + + let mut tasks = smallvec![]; + + tasks.push( + // Establish connection to the bootstrap relays + // + // And handle notifications from the nostr relay pool channel + cx.background_spawn({ + let client = client.clone(); + let gossip = Arc::clone(&gossip); + let tracker = Arc::clone(&tracker); + let _ = initialized_at(); + + async move { + // Connect to the bootstrap relays + Self::connect(&client).await; + + // Handle notifications from the relay pool + Self::handle_notifications(&client, &gossip, &tracker).await; + } + }), + ); + + Self { + client, + tracker, + gossip, + _tasks: tasks, + } + } + + /// Establish connection to the bootstrap relays + async fn connect(client: &Client) { + // Get all bootstrapping relays + let mut urls = vec![]; + urls.extend(BOOTSTRAP_RELAYS); + urls.extend(SEARCH_RELAYS); + + // Add relay to the relay pool + for url in urls.into_iter() { + client.add_relay(url).await.ok(); + } + + // Connect to all added relays + client.connect().await; + } + + async fn handle_notifications( + client: &Client, + gossip: &Arc>, + tracker: &Arc>, + ) { + let mut notifications = client.notifications(); + let mut processed_events = HashSet::new(); + + while let Ok(notification) = notifications.recv().await { + let RelayPoolNotification::Message { message, relay_url } = notification else { + // Skip if the notification is not a message + continue; + }; + + match message { + RelayMessage::Event { event, .. } => { + if !processed_events.insert(event.id) { + // Skip if the event has already been processed + continue; + } + + match event.kind { + Kind::RelayList => { + let mut gossip = gossip.write().await; + gossip.insert_relays(&event); + + let urls: Vec = Self::extract_write_relays(&event); + let author = event.pubkey; + + log::info!("Write relays: {urls:?}"); + + // 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::get(client, &urls, author, Kind::Metadata).await; + // Fetch user's contact list event + Self::get(client, &urls, author, Kind::ContactList).await; + } + } + Kind::InboxRelays => { + let mut gossip = gossip.write().await; + gossip.insert_messaging_relays(&event); + + if Self::is_self_authored(client, &event).await { + // Extract user's messaging relays + let urls: Vec = + nip17::extract_relay_list(&event).cloned().collect(); + + // Fetch user's inbox messages in the extracted relays + Self::get_messages(client, event.pubkey, &urls).await; + } + } + Kind::Custom(10044) => { + let mut gossip = gossip.write().await; + gossip.insert_announcement(&event); + } + Kind::ContactList => { + if Self::is_self_authored(client, &event).await { + let public_keys: Vec = + event.tags.public_keys().copied().collect(); + + if let Err(e) = + Self::get_metadata_for_list(client, public_keys).await + { + log::error!("Failed to get metadata for list: {e}"); + } + } + } + _ => {} + }; + } + RelayMessage::Ok { + event_id, message, .. + } => { + let msg = MachineReadablePrefix::parse(&message); + let mut tracker = tracker.write().await; + + // Message that need to be authenticated will be handled separately + if let Some(MachineReadablePrefix::AuthRequired) = msg { + // Keep track of events that need to be resent after authentication + tracker.resend_queue.insert(event_id, relay_url); + } else { + // Keep track of events sent by Coop + tracker.sent_ids.insert(event_id); + } + } + _ => {} + } + } + } + + /// Check if event is published by current user + pub async fn is_self_authored(client: &Client, event: &Event) -> bool { + if let Ok(signer) = client.signer().await { + if let Ok(public_key) = signer.get_public_key().await { + return public_key == event.pubkey; + } + } + false + } + + /// 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 + 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 + 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); + + // Unsubscribe from the previous subscription + client.unsubscribe(&id).await; + + // Subscribe to filters to user's messaging relays + 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]; + + // Return if the list is empty + if pubkeys.is_empty() { + return Err(anyhow!("You need at least one public key".to_string(),)); + } + + let filter = Filter::new() + .limit(pubkeys.len() * kinds.len()) + .authors(pubkeys) + .kinds(kinds); + + // Subscribe to filters to the bootstrap relays + client + .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) + .await?; + + Ok(()) + } + + pub fn extract_read_relays(event: &Event) -> Vec { + nip65::extract_relay_list(event) + .filter_map(|(url, metadata)| { + if metadata.is_none() || metadata == &Some(RelayMetadata::Read) { + Some(url.to_owned()) + } else { + None + } + }) + .take(3) + .collect() + } + + pub 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 + .tags + .iter() + .find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey") + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .context("Cannot parse public key from the event's tags")?; + + let client_name = event + .tags + .find(TagKind::Client) + .and_then(|tag| tag.content()) + .map(|c| c.to_string()); + + Ok(Announcement::new(event.id, client_name, public_key)) + } + + /// Extract an encryption keys response from an event. + pub async fn extract_response(client: &Client, event: &Event) -> Result { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + if event.pubkey != public_key { + return Err(anyhow!("Event does not belong to current user")); + } + + let client_pubkey = event + .tags + .find(TagKind::custom("P")) + .and_then(|tag| tag.content()) + .and_then(|c| PublicKey::parse(c).ok()) + .context("Cannot parse public key from the event's tags")?; + + Ok(Response::new(event.content.clone(), client_pubkey)) + } + + /// Returns a reference to the nostr client. + pub fn client(&self) -> Client { + self.client.clone() + } + + /// Returns a reference to the event tracker. + pub fn tracker(&self) -> Arc> { + Arc::clone(&self.tracker) + } + + /// Returns a reference to the cache manager. + pub fn gossip(&self) -> Arc> { + Arc::clone(&self.gossip) + } +} diff --git a/crates/state/src/storage.rs b/crates/state_old/src/storage.rs similarity index 100% rename from crates/state/src/storage.rs rename to crates/state_old/src/storage.rs diff --git a/crates/state/src/tracker.rs b/crates/state_old/src/tracker.rs similarity index 100% rename from crates/state/src/tracker.rs rename to crates/state_old/src/tracker.rs