diff --git a/Cargo.lock b/Cargo.lock index a5fe820..4c9ca19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1465,7 +1465,6 @@ dependencies = [ "assets", "auto_update", "chat", - "chat_ui", "common", "device", "futures", @@ -1480,7 +1479,6 @@ dependencies = [ "nostr-sdk", "oneshot", "person", - "relay_auth", "reqwest_client", "serde", "serde_json", @@ -1514,7 +1512,6 @@ dependencies = [ "log", "nostr-sdk", "person", - "relay_auth", "settings", "state", "theme", @@ -5939,23 +5936,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "relay_auth" -version = "1.0.0-beta5" -dependencies = [ - "anyhow", - "common", - "flume 0.11.1", - "gpui", - "log", - "nostr-sdk", - "settings", - "smallvec", - "state", - "theme", - "ui", -] - [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -9224,7 +9204,6 @@ dependencies = [ "nostr-sdk", "oneshot", "person", - "relay_auth", "serde", "serde_json", "settings", diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 8052b04..b3f4eee 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -19,7 +19,7 @@ use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; #[cfg(not(target_arch = "wasm32"))] use smol::lock::RwLock; -use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP}; +use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner}; mod message; mod room; @@ -140,17 +140,15 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let user_signer = nostr.read(cx).signer.clone(); - let (tx, rx) = flume::unbounded::(); let mut subscriptions = smallvec![]; subscriptions.push( // Subscribe to the signer event - cx.observe(&user_signer, |this, signer, cx| { - if let Some(keys) = signer.read(cx).clone() { + cx.subscribe(&nostr, |this, _nostr, event, cx| { + if event.signer_changed() { this.reset(cx); - this.handle_notifications(keys, cx); + this.handle_notifications(cx); this.get_metadata(cx); this.get_rooms(cx); }; @@ -178,9 +176,10 @@ impl ChatRegistry { } /// Handle nostr notifications - fn handle_notifications(&mut self, signer: Keys, cx: &mut Context) { + fn handle_notifications(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); let tracking = self.tracking.clone(); let msg_relays_existed = self.msg_relays_existed.clone(); @@ -340,7 +339,7 @@ impl ChatRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -392,11 +391,9 @@ impl ChatRegistry { fn get_messages(&mut self, msg_relays: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let urls: Vec = nip17::extract_relay_list(msg_relays).collect(); + let signer = nostr.read(cx).signer(); - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let urls: Vec = nip17::extract_relay_list(msg_relays).collect(); let task: Task> = cx.background_spawn(async move { let public_key = signer.get_public_key_async().await?; @@ -514,7 +511,7 @@ impl ChatRegistry { { let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -625,7 +622,7 @@ impl ChatRegistry { pub fn get_rooms(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -735,7 +732,7 @@ impl ChatRegistry { pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -771,7 +768,7 @@ impl ChatRegistry { /// Unwraps a gift-wrapped event and processes its contents. async fn extract_rumor( client: &Client, - signer: &Keys, + signer: &UniversalSigner, gift_wrap: &Event, ) -> Result { // Try to get cached rumor first @@ -795,7 +792,7 @@ async fn extract_rumor( } /// Helper method to try unwrapping with different signers -async fn try_unwrap(signer: &Keys, gift_wrap: &Event) -> Result { +async fn try_unwrap(signer: &UniversalSigner, gift_wrap: &Event) -> Result { /* * // Try with the device signer first if let Some(signer) = signer.get_encryption_signer().await { @@ -808,13 +805,16 @@ async fn try_unwrap(signer: &Keys, gift_wrap: &Event) -> Result Result { +async fn try_unwrap_with( + signer: &UniversalSigner, + gift_wrap: &Event, +) -> Result { // Get the sealed event let seal = signer .nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content) diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index 50c5827..9971a85 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -10,7 +10,7 @@ use itertools::Itertools; use nostr_sdk::prelude::*; use person::{Person, PersonRegistry}; use settings::{RoomConfig, SignerKind}; -use state::{NostrRegistry, TIMEOUT}; +use state::{NostrRegistry, TIMEOUT, UniversalSigner}; use crate::NewMessage; @@ -427,7 +427,7 @@ impl Room { let nostr = NostrRegistry::global(cx); // Get current user's public key - let sender = nostr.read(cx).signer_pubkey(cx)?; + let sender = nostr.read(cx).current_user()?; // Get all members, excluding the sender let members: Vec = self @@ -482,13 +482,12 @@ impl Room { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let user_signer = nostr.read(cx).signer(); + let current_user = nostr.read(cx).current_user()?; - // Get current user's public key - let user_signer = nostr.read(cx).signer(cx)?; - let public_key = nostr.read(cx).signer_pubkey(cx)?; - + // Get sender's profile let persons = PersonRegistry::global(cx); - let sender = persons.read(cx).get(&public_key, cx); + let sender = persons.read(cx).get(¤t_user, cx); // Get all members (excluding sender) let members: Vec = self @@ -597,7 +596,7 @@ impl Room { // Helper function to send a gift-wrapped event async fn send_gift_wrap( client: &Client, - signer: &Keys, + signer: &UniversalSigner, receiver: &Person, rumor: &UnsignedEvent, config: &SignerKind, diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index d719216..5c06f1c 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -15,7 +15,7 @@ use nostr_sdk::prelude::*; use person::PersonRegistry; use settings::AppSettings; use smallvec::{SmallVec, smallvec}; -use state::{Announcement, CLIENT_NAME, NostrRegistry}; +use state::{Announcement, CLIENT_NAME, NostrRegistry, UniversalSigner}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::Button; @@ -66,7 +66,7 @@ pub struct DeviceRegistry { pub announcement_existed: Arc, /// Signer - signer: Entity>, + signer: Entity>, /// Async tasks tasks: Vec>>, @@ -90,14 +90,11 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let signer = cx.new(|_| None); - let nostr = NostrRegistry::global(cx); - let user_signer = nostr.read(cx).signer.clone(); - let settings = AppSettings::global(cx); - let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); + let nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); + let signer = cx.new(|_| None); let mut subscriptions = smallvec![]; subscriptions.push( @@ -111,10 +108,10 @@ impl DeviceRegistry { subscriptions.push( // Observe the user signer - cx.observe(&user_signer, move |this, signer, cx| { - if signer.read(cx).is_some() && is_nip4e_enabled { + cx.subscribe(&nostr, move |this, _nostr, event, cx| { + if event.signer_changed() && nip4e_enabled { this.get_announcement(cx); - }; + } }), ); @@ -134,7 +131,7 @@ impl DeviceRegistry { fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let current_user = nostr.read(cx).signer_pubkey(cx); + let signer = nostr.read(cx).signer(); let announcement_existed = self.announcement_existed.clone(); let (tx, rx) = flume::bounded::(100); @@ -142,6 +139,7 @@ impl DeviceRegistry { self.tasks.push(cx.background_spawn(async move { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); + let current_user = signer.get_public_key_async().await.ok(); while let Some(notification) = notifications.next().await { if let ClientNotification::Message { message, .. } = notification @@ -205,14 +203,14 @@ impl DeviceRegistry { } /// Get the signer - pub fn signer(&self, cx: &App) -> Option { + pub fn signer(&self, cx: &App) -> Option { self.signer.read(cx).clone() } /// Set the decoupled encryption key for the current user - fn set_signer(&mut self, new: Keys, cx: &mut Context) { + fn set_signer(&mut self, new_signer: Keys, cx: &mut Context) { self.signer.update(cx, |this, cx| { - *this = Some(new); + *this = Some(UniversalSigner::new(new_signer)); cx.notify(); }); cx.emit(DeviceEvent::Set); @@ -222,10 +220,7 @@ impl DeviceRegistry { pub fn backup(&self, path: PathBuf, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return Task::ready(Err(anyhow!("Signer is required"))); - }; + let signer = nostr.read(cx).signer(); cx.background_spawn(async move { let keys = get_keys(&client, &signer).await?; @@ -241,13 +236,11 @@ impl DeviceRegistry { pub fn get_announcement(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); self.tasks.push(cx.background_spawn(async move { let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + let current_user = signer.get_public_key_async().await?; // Construct the filter for the device announcement event let filter = Filter::new() @@ -317,14 +310,11 @@ impl DeviceRegistry { fn create_encryption(&self, keys: Keys, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); let secret = keys.secret_key().to_secret_hex(); let n = keys.public_key(); - let Some(signer) = nostr.read(cx).signer(cx) else { - return Task::ready(Err(anyhow!("Signer is required"))); - }; - cx.background_spawn(async move { // Construct an announcement event let event = EventBuilder::new(Kind::Custom(10044), "") @@ -353,10 +343,7 @@ impl DeviceRegistry { fn set_encryption(&mut self, event: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); let announcement = Announcement::from(event); let device_pubkey = announcement.public_key(); @@ -392,10 +379,7 @@ impl DeviceRegistry { fn wait_for_request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); self.tasks.push(cx.background_spawn(async move { let public_key = signer.get_public_key_async().await?; @@ -418,10 +402,7 @@ impl DeviceRegistry { pub fn request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); let Ok(app_keys) = get_or_init_app_keys(cx) else { return; @@ -486,10 +467,7 @@ impl DeviceRegistry { fn wait_for_approval(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); cx.emit(DeviceEvent::Requesting); @@ -554,10 +532,7 @@ impl DeviceRegistry { fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); // Get user's write relays let event = event.clone(); @@ -770,7 +745,7 @@ fn get_or_init_app_keys(cx: &App) -> Result { } /// Encrypt and store device keys in the local database. -async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Error> { +async fn set_keys(client: &Client, signer: &UniversalSigner, secret: &str) -> Result<(), Error> { let public_key = signer.get_public_key_async().await?; let content = signer.nip44_encrypt_async(&public_key, secret).await?; @@ -787,7 +762,7 @@ async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Er } /// Get device keys from the local database. -async fn get_keys(client: &Client, signer: &Keys) -> Result { +async fn get_keys(client: &Client, signer: &UniversalSigner) -> Result { let public_key = signer.get_public_key_async().await?; let filter = Filter::new() diff --git a/crates/relay_auth/Cargo.toml b/crates/relay_auth/Cargo.toml deleted file mode 100644 index d532a52..0000000 --- a/crates/relay_auth/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "relay_auth" -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 -flume.workspace = true -log.workspace = true diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs deleted file mode 100644 index 8d32e61..0000000 --- a/crates/relay_auth/src/lib.rs +++ /dev/null @@ -1,372 +0,0 @@ -use std::borrow::Cow; -use std::cell::Cell; -use std::collections::HashSet; -use std::hash::Hash; -use std::rc::Rc; -use std::sync::Arc; - -use anyhow::{Context as AnyhowContext, Error, anyhow}; -use gpui::{ - App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, Styled, - Task, Window, div, relative, -}; -use nostr_sdk::prelude::*; -use settings::{AppSettings, AuthMode}; -use smallvec::{SmallVec, smallvec}; -use state::NostrRegistry; -use theme::ActiveTheme; -use ui::button::Button; -use ui::notification::{Notification, NotificationKind}; -use ui::{Disableable, WindowExtension, v_flex}; - -const AUTH_MESSAGE: &str = - "Approve the authentication request to allow Coop to continue sending or receiving events."; - -pub fn init(window: &mut Window, cx: &mut App) { - RelayAuth::set_global(cx.new(|cx| RelayAuth::new(window, cx)), cx); -} - -/// Authentication request -#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] -struct AuthRequest { - url: RelayUrl, - challenge: String, -} - -impl AuthRequest { - pub fn new(challenge: S, url: RelayUrl) -> Self - where - S: Into, - { - Self { - challenge: challenge.into(), - url, - } - } - - pub fn url(&self) -> &RelayUrl { - &self.url - } - - pub fn challenge(&self) -> &str { - &self.challenge - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -enum Signal { - Auth(Arc), - Pending((EventId, RelayUrl)), -} - -struct GlobalRelayAuth(Entity); - -impl Global for GlobalRelayAuth {} - -// Relay authentication -#[derive(Debug)] -pub struct RelayAuth { - /// Pending events waiting for resend after authentication - pending_events: HashSet<(EventId, RelayUrl)>, - - /// Tasks for asynchronous operations - _tasks: SmallVec<[Task<()>; 2]>, -} - -impl RelayAuth { - /// Retrieve the global relay auth state - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - /// Set the global relay auth instance - fn set_global(state: Entity, cx: &mut App) { - cx.set_global(GlobalRelayAuth(state)); - } - - /// Create a new relay auth instance - fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let mut tasks = smallvec![]; - - // Channel for communication between nostr and gpui - let (tx, rx) = flume::bounded::(256); - - tasks.push(cx.background_spawn(async move { - let mut notifications = client.notifications(); - let mut challenges: HashSet> = HashSet::default(); - - while let Some(notification) = notifications.next().await { - if let ClientNotification::Message { relay_url, message } = notification { - match *message { - RelayMessage::Auth { challenge } => { - if challenges.insert(challenge.clone()) { - let request = Arc::new(AuthRequest::new(challenge, relay_url)); - let signal = Signal::Auth(request); - - tx.send_async(signal).await.ok(); - } - } - RelayMessage::Ok { - event_id, message, .. - } => { - let msg = MachineReadablePrefix::parse(&message); - - // Handle authentication messages - if let Some(MachineReadablePrefix::AuthRequired) = msg { - let signal = Signal::Pending((event_id, relay_url)); - tx.send_async(signal).await.ok(); - } - } - _ => {} - } - } - } - })); - - tasks.push(cx.spawn_in(window, async move |this, cx| { - while let Ok(signal) = rx.recv_async().await { - match signal { - Signal::Auth(req) => { - this.update_in(cx, |this, window, cx| { - this.handle_auth(&req, window, cx); - }) - .ok(); - } - Signal::Pending((event_id, relay_url)) => { - this.update_in(cx, |this, _window, cx| { - this.insert_pending_event(event_id, relay_url, cx); - }) - .ok(); - } - } - } - })); - - Self { - pending_events: HashSet::default(), - _tasks: tasks, - } - } - - /// Insert a pending event waiting for resend after authentication - fn insert_pending_event(&mut self, id: EventId, relay: RelayUrl, cx: &mut Context) { - self.pending_events.insert((id, relay)); - cx.notify(); - } - - /// Get all pending events for a specific relay, - fn get_pending_events(&self, relay: &RelayUrl, _cx: &App) -> Vec { - self.pending_events - .iter() - .filter(|(_, pending_relay)| pending_relay == relay) - .map(|(id, _relay)| id) - .cloned() - .collect() - } - - /// Clear all pending events for a specific relay, - fn clear_pending_events(&mut self, relay: &RelayUrl, cx: &mut Context) { - self.pending_events - .retain(|(_, pending_relay)| pending_relay != relay); - cx.notify(); - } - - /// Handle authentication request - fn handle_auth(&mut self, req: &Arc, window: &mut Window, cx: &mut Context) { - let settings = AppSettings::global(cx); - let trusted_relay = settings.read(cx).trusted_relay(req.url(), cx); - let mode = AppSettings::get_auth_mode(cx); - - if trusted_relay && mode == AuthMode::Auto { - // Automatically authenticate if the relay is authenticated before - self.response(req, window, cx); - } else { - // Otherwise open the auth request popup - self.ask_for_approval(req, window, cx); - } - } - - /// Send auth response and wait for confirmation - fn auth(&self, req: &Arc, cx: &App) -> Task> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return Task::ready(Err(anyhow!("Signer is required"))); - }; - - // Get all pending events for the relay - let req = req.clone(); - let pending_events = self.get_pending_events(req.url(), cx); - - cx.background_spawn(async move { - // Construct event - let event = EventBuilder::auth(req.challenge(), req.url().clone()) - .finalize_async(&signer) - .await?; - - // Get the event ID - let id = event.id; - - // Get the relay - let relay = client.relay(req.url()).await?.context("Relay not found")?; - - // Subscribe to notifications - let mut notifications = relay.notifications(); - - // Send the AUTH message - relay - .send_msg(ClientMessage::Auth(Cow::Borrowed(&event))) - .await?; - - while let Some(notification) = notifications.next().await { - match notification { - RelayNotification::Message { message } => { - if let RelayMessage::Ok { event_id, .. } = *message { - if id != event_id { - continue; - } - - // Get all subscriptions - let subscriptions = relay.subscriptions().await; - - // Re-subscribe to previous subscriptions - for (id, filters) in subscriptions.into_iter() { - if !filters.is_empty() { - relay.send_msg(ClientMessage::req(id, filters)).await?; - } - } - - // Re-send pending events - for id in pending_events { - if let Some(event) = client.database().event_by_id(&id).await? { - relay.send_event(&event).await?; - } - } - - return Ok(()); - } - } - RelayNotification::AuthenticationFailed => break, - _ => {} - } - } - - Err(anyhow!("Authentication failed")) - }) - } - - /// Respond to an authentication request. - fn response(&self, req: &Arc, window: &Window, cx: &Context) { - let settings = AppSettings::global(cx); - let req = req.clone(); - let challenge = SharedString::from(req.challenge().to_string()); - - // Create a task for authentication - let task = self.auth(&req, cx); - - cx.spawn_in(window, async move |this, cx| { - let result = task.await; - let url = req.url(); - - this.update_in(cx, |this, window, cx| { - window.clear_notification_by_id::(challenge, cx); - - if let Err(e) = result { - window - .push_notification(Notification::error(e.to_string()).autohide(false), cx); - } else { - // Clear pending events for the authenticated relay - this.clear_pending_events(url, cx); - - let domain = url.domain().unwrap_or_default(); - let msg = format!("Relay {} has been authenticated", domain); - - window.push_notification(Notification::success(msg), cx); - - // Save the authenticated relay to automatically authenticate future requests - settings.update(cx, |this, cx| { - this.add_trusted_relay(url, cx); - }); - } - }) - .ok(); - }) - .detach(); - } - - /// Push a popup to approve the authentication request. - fn ask_for_approval(&self, req: &Arc, window: &Window, cx: &Context) { - let notification = self.notification(req, cx); - - cx.spawn_in(window, async move |_this, cx| { - cx.update(|window, cx| { - window.push_notification(notification, cx); - }) - .ok(); - }) - .detach(); - } - - /// Build a notification for the authentication request. - fn notification(&self, req: &Arc, cx: &Context) -> Notification { - let req = req.clone(); - let challenge = SharedString::from(req.challenge.clone()); - let url = SharedString::from(req.url().to_string()); - let entity = cx.entity().downgrade(); - let loading = Rc::new(Cell::new(false)); - - Notification::new() - .type_id::(challenge) - .autohide(false) - .with_kind(NotificationKind::Info) - .title("Authentication Required") - .content(move |_this, _window, cx| { - v_flex() - .gap_2() - .child( - div() - .text_sm() - .line_height(relative(1.25)) - .child(SharedString::from(AUTH_MESSAGE)), - ) - .child( - v_flex() - .py_1() - .px_1p5() - .rounded_sm() - .text_xs() - .bg(cx.theme().elevated_surface_background) - .text_color(cx.theme().text) - .child(url.clone()), - ) - .into_any_element() - }) - .action(move |_this, _window, _cx| { - let view = entity.clone(); - let req = req.clone(); - - Button::new("approve") - .label("Approve") - .loading(loading.get()) - .disabled(loading.get()) - .on_click({ - let loading = Rc::clone(&loading); - move |_ev, window, cx| { - // Set loading state to true - loading.set(true); - // Process to approve the request - view.update(cx, |this, cx| { - this.response(&req, window, cx); - }) - .ok(); - } - }) - }) - } -} - -struct AuthNotification; diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 24d8d02..827ba0a 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -45,7 +45,7 @@ pub const SEARCH_RELAYS: [&str; 2] = ["wss://antiprimal.net", "wss://search.nos. /// Default bootstrap relays pub const BOOTSTRAP_RELAYS: [&str; 3] = [ - "wss://relay.damus.io", + "wss://relay.ditto.pub", "wss://relay.primal.net", "wss://user.kindpag.es", ]; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 3ca8d2c..b7bea9c 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -15,11 +15,13 @@ mod blossom; mod constants; mod nip05; mod nip4e; +mod signer; pub use blossom::*; pub use constants::*; pub use nip4e::*; pub use nip05::*; +pub use signer::UniversalSigner; pub fn init(window: &mut Window, cx: &mut App) { // rustls uses the `aws_lc_rs` provider by default @@ -44,6 +46,8 @@ impl Global for GlobalNostrRegistry {} /// Signer event. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum StateEvent { + /// The signer has changed + SignerChanged, /// Connecting to the bootstrapping relay Connecting, /// Connected to the bootstrapping relay @@ -59,6 +63,10 @@ impl StateEvent { { Self::Error(error.into()) } + + pub fn signer_changed(&self) -> bool { + matches!(self, StateEvent::SignerChanged) + } } /// Nostr Registry @@ -67,8 +75,11 @@ pub struct NostrRegistry { /// Nostr client client: Client, - /// Currently active signer - pub signer: Entity>, + /// Universal signer + signer: UniversalSigner, + + /// Current user's public key + current_user: Option, /// Tasks for asynchronous operations tasks: Vec>>, @@ -89,7 +100,8 @@ impl NostrRegistry { /// Create a new nostr instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let signer = cx.new(|_| None); + let signer = UniversalSigner::new(Keys::generate()); + let authenticator = SignerAuthenticator::new(signer.clone()); // Construct the nostr lmdb instance #[cfg(not(target_arch = "wasm32"))] @@ -105,6 +117,7 @@ impl NostrRegistry { // Construct the nostr client let client = ClientBuilder::default() .database(database) + .authenticator(authenticator) .gossip(NostrGossipMemory::unbounded()) .gossip_config(GossipConfig::default().no_background_refresh()) .connect_timeout(Duration::from_secs(10)) @@ -121,6 +134,7 @@ impl NostrRegistry { Self { client, signer, + current_user: None, tasks: vec![], } } @@ -130,22 +144,33 @@ impl NostrRegistry { self.client.clone() } - /// Get the signer - pub fn signer(&self, cx: &App) -> Option { - self.signer.read(cx).clone() + /// Get the current signer + pub fn signer(&self) -> UniversalSigner { + self.signer.clone() } - /// Get the public key of the signer - pub fn signer_pubkey(&self, cx: &App) -> Option { - self.signer.read(cx).as_ref().map(|s| s.public_key()) + /// Get the current user's public key + pub fn current_user(&self) -> Option { + self.current_user } - /// Set the signer to the given keys - pub fn set_signer(&mut self, new_keys: Keys, cx: &mut Context) { - self.signer.update(cx, |this, cx| { - *this = Some(new_keys); - cx.notify(); - }); + /// Update the signer + pub fn set_signer(&mut self, new_signer: T, cx: &mut Context) + where + T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static, + ::Error: std::error::Error + Send + Sync + 'static, + ::Error: std::error::Error + Send + Sync + 'static, + ::Error: std::error::Error + Send + Sync + 'static, + { + cx.spawn(async move |this, cx| { + let current_user = new_signer.get_public_key_async().await.ok(); + this.update(cx, |this, cx| { + this.current_user = current_user; + cx.emit(StateEvent::SignerChanged); + }) + .ok(); + }) + .detach(); } /// Connect to the bootstrapping relays @@ -299,10 +324,7 @@ impl NostrRegistry { pub fn wot_search(&self, query: &str, cx: &App) -> Task, Error>> { let client = self.client(); let query = query.to_string(); - - let Some(signer) = self.signer.read(cx).clone() else { - return Task::ready(Err(anyhow!("Signer is required"))); - }; + let signer = self.signer.clone(); cx.background_spawn(async move { // Construct a vertex request event diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs new file mode 100644 index 0000000..4a48348 --- /dev/null +++ b/crates/state/src/signer.rs @@ -0,0 +1,182 @@ +use std::error::Error; +use std::fmt; +use std::sync::{Arc, RwLock}; + +use nostr_sdk::prelude::*; + +#[derive(Debug)] +pub struct UniversalSignerError(Box); + +impl fmt::Display for UniversalSignerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl Error for UniversalSignerError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(&*self.0) + } +} + +impl UniversalSignerError { + pub fn new(err: E) -> Self + where + E: Error + Send + Sync + 'static, + { + UniversalSignerError(Box::new(err)) + } +} + +#[derive(Clone, Debug)] +pub struct UniversalSigner { + inner: Arc>>, +} + +impl UniversalSigner { + pub fn new(signer: T) -> Self + where + T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static, + ::Error: Error + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, + { + Self { + inner: Arc::new(RwLock::new(Arc::new(InnerSignerImpl(signer)))), + } + } + + /// Swap the inner signer in-place. All clones see the new signer. + pub fn swap_inner(&self, new_signer: T) + where + T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + 'static, + ::Error: Error + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, + { + *self.inner.write().expect("RwLock poisoned") = Arc::new(InnerSignerImpl(new_signer)); + } +} + +trait InnerSigner: fmt::Debug + Send + Sync + 'static { + fn get_public_key_async(&self) -> BoxedFuture<'_, Result>; + fn sign_event_async( + &self, + unsigned: UnsignedEvent, + ) -> BoxedFuture<'_, Result>; + fn nip44_encrypt_async<'a>( + &'a self, + public_key: &'a PublicKey, + content: &'a str, + ) -> BoxedFuture<'a, Result>; + fn nip44_decrypt_async<'a>( + &'a self, + public_key: &'a PublicKey, + payload: &'a str, + ) -> BoxedFuture<'a, Result>; +} + +#[derive(Debug)] +struct InnerSignerImpl(T); + +impl InnerSigner for InnerSignerImpl +where + T: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, + ::Error: Error + Send + Sync + 'static, +{ + fn get_public_key_async(&self) -> BoxedFuture<'_, Result> { + Box::pin(async move { + AsyncGetPublicKey::get_public_key_async(&self.0) + .await + .map_err(UniversalSignerError::new) + }) + } + + fn sign_event_async( + &self, + unsigned: UnsignedEvent, + ) -> BoxedFuture<'_, Result> { + Box::pin(async move { + AsyncSignEvent::sign_event_async(&self.0, unsigned) + .await + .map_err(UniversalSignerError::new) + }) + } + + fn nip44_encrypt_async<'a>( + &'a self, + public_key: &'a PublicKey, + content: &'a str, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + AsyncNip44::nip44_encrypt_async(&self.0, public_key, content) + .await + .map_err(UniversalSignerError::new) + }) + } + + fn nip44_decrypt_async<'a>( + &'a self, + public_key: &'a PublicKey, + payload: &'a str, + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { + AsyncNip44::nip44_decrypt_async(&self.0, public_key, payload) + .await + .map_err(UniversalSignerError::new) + }) + } +} + +impl UniversalSigner { + #[allow(dead_code)] + fn with_inner(&self, f: impl FnOnce(&dyn InnerSigner) -> R) -> R { + let guard = self.inner.read().expect("RwLock poisoned"); + f(&**guard) + } +} + +impl AsyncGetPublicKey for UniversalSigner { + type Error = UniversalSignerError; + + fn get_public_key_async(&self) -> BoxedFuture<'_, Result> { + let inner = self.inner.read().expect("RwLock poisoned").clone(); + Box::pin(async move { inner.get_public_key_async().await }) + } +} + +impl AsyncSignEvent for UniversalSigner { + type Error = UniversalSignerError; + + fn sign_event_async( + &self, + unsigned: UnsignedEvent, + ) -> BoxedFuture<'_, Result> { + let inner = self.inner.read().expect("RwLock poisoned").clone(); + Box::pin(async move { inner.sign_event_async(unsigned).await }) + } +} + +impl AsyncNip44 for UniversalSigner { + type Error = UniversalSignerError; + + fn nip44_encrypt_async<'a>( + &'a self, + public_key: &'a PublicKey, + content: &'a str, + ) -> BoxedFuture<'a, Result> { + let inner = self.inner.read().expect("RwLock poisoned").clone(); + Box::pin(async move { inner.nip44_encrypt_async(public_key, content).await }) + } + + fn nip44_decrypt_async<'a>( + &'a self, + public_key: &'a PublicKey, + payload: &'a str, + ) -> BoxedFuture<'a, Result> { + let inner = self.inner.read().expect("RwLock poisoned").clone(); + Box::pin(async move { inner.nip44_decrypt_async(public_key, payload).await }) + } +} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index dd1fc82..8bc47a9 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -15,7 +15,6 @@ chat_ui = { path = "../chat_ui" } settings = { path = "../settings" } auto_update = { path = "../auto_update" } person = { path = "../person" } -relay_auth = { path = "../relay_auth" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/workspace/src/dialogs/screening.rs b/crates/workspace/src/dialogs/screening.rs index 3f5b375..45a61a0 100644 --- a/crates/workspace/src/dialogs/screening.rs +++ b/crates/workspace/src/dialogs/screening.rs @@ -78,7 +78,7 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; - let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + let Some(current_user) = nostr.read(cx).current_user() else { return; }; @@ -106,7 +106,7 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; - let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + let Some(current_user) = nostr.read(cx).current_user() else { return; }; @@ -224,12 +224,9 @@ impl Screening { fn report(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); let public_key = self.public_key; - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; - let task: Task> = cx.background_spawn(async move { let tag = Nip56Tag::PublicKey { public_key, diff --git a/crates/workspace/src/lib.rs b/crates/workspace/src/lib.rs index fa2934f..98f17b8 100644 --- a/crates/workspace/src/lib.rs +++ b/crates/workspace/src/lib.rs @@ -75,7 +75,6 @@ impl Workspace { let chat = ChatRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer.clone(); let dock = cx.new(|cx| DockArea::new(window, cx)); let image_cache = CoopImageCache::new(IMAGE_CACHE_SIZE, cx); @@ -89,20 +88,9 @@ impl Workspace { }), ); - subscriptions.push( - // Observe the signer - cx.observe_in(&signer, window, |this, signer, window, cx| { - if signer.read(cx).is_some() { - this.set_center_layout(window, cx); - } else { - this.import_identity(window, cx); - } - }), - ); - subscriptions.push( // Subscribe to the nostr events - cx.subscribe_in(&nostr, window, move |this, state, event, window, cx| { + cx.subscribe_in(&nostr, window, move |_this, _state, event, window, cx| { match event { StateEvent::Connecting => { let note = Notification::new() @@ -119,10 +107,6 @@ impl Workspace { .with_kind(NotificationKind::Success); window.push_notification(note, cx); - - if state.read(cx).signer.read(cx).is_none() { - this.import_identity(window, cx); - } } _ => {} }; @@ -330,7 +314,7 @@ impl Workspace { Command::ShowProfile => { let nostr = NostrRegistry::global(cx); - if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) { + if let Some(public_key) = nostr.read(cx).current_user() { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(profile::init(public_key, window, cx)), @@ -583,7 +567,7 @@ impl Workspace { fn titlebar_left(&mut self, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); - let current_user = nostr.read(cx).signer_pubkey(cx); + let current_user = nostr.read(cx).current_user(); h_flex() .flex_shrink_0() @@ -661,7 +645,7 @@ impl Workspace { let is_nip4e_enabled = AppSettings::get_nip4e(cx); let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return div(); }; diff --git a/crates/workspace/src/panels/contact_list.rs b/crates/workspace/src/panels/contact_list.rs index 7db2a29..47188e1 100644 --- a/crates/workspace/src/panels/contact_list.rs +++ b/crates/workspace/src/panels/contact_list.rs @@ -82,7 +82,7 @@ impl ContactListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -157,10 +157,7 @@ impl ContactListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); // Get contacts let contacts: Vec = self diff --git a/crates/workspace/src/panels/greeter.rs b/crates/workspace/src/panels/greeter.rs index c56e8d1..69ddad0 100644 --- a/crates/workspace/src/panels/greeter.rs +++ b/crates/workspace/src/panels/greeter.rs @@ -31,7 +31,7 @@ impl GreeterPanel { fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) { + if let Some(public_key) = nostr.read(cx).current_user() { cx.spawn_in(window, async move |_this, cx| { cx.update(|window, cx| { Workspace::add_panel( diff --git a/crates/workspace/src/panels/messaging_relays.rs b/crates/workspace/src/panels/messaging_relays.rs index 284c92c..b89ca81 100644 --- a/crates/workspace/src/panels/messaging_relays.rs +++ b/crates/workspace/src/panels/messaging_relays.rs @@ -83,7 +83,7 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -171,10 +171,7 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); // Construct event tags let tags: Vec = self diff --git a/crates/workspace/src/panels/profile.rs b/crates/workspace/src/panels/profile.rs index 378a91c..d8da358 100644 --- a/crates/workspace/src/panels/profile.rs +++ b/crates/workspace/src/panels/profile.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Context as AnyhowContext, Error}; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, @@ -207,12 +207,9 @@ impl ProfilePanel { fn publish(&self, metadata: &Metadata, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); let metadata = metadata.clone(); - let Some(signer) = nostr.read(cx).signer(cx) else { - return Task::ready(Err(anyhow!("Signer is required"))); - }; - cx.background_spawn(async move { // Build and sign the metadata event let event = metadata.finalize_async(&signer).await?; diff --git a/crates/workspace/src/panels/relay_list.rs b/crates/workspace/src/panels/relay_list.rs index 26d4237..df3eabe 100644 --- a/crates/workspace/src/panels/relay_list.rs +++ b/crates/workspace/src/panels/relay_list.rs @@ -100,7 +100,7 @@ impl RelayListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -207,10 +207,7 @@ impl RelayListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - - let Some(signer) = nostr.read(cx).signer(cx) else { - return; - }; + let signer = nostr.read(cx).signer(); // Get all relays let relays = self.relays.clone(); diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index fa5598f..32705ad 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -159,7 +159,7 @@ impl Sidebar { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; @@ -320,7 +320,7 @@ impl Sidebar { let async_chat = chat.downgrade(); let nostr = NostrRegistry::global(cx); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + let Some(public_key) = nostr.read(cx).current_user() else { return; }; diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index b5627d7..e08ba5b 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -35,11 +35,9 @@ common = { path = "../crates/common" } state = { path = "../crates/state" } device = { path = "../crates/device" } chat = { path = "../crates/chat" } -chat_ui = { path = "../crates/chat_ui" } settings = { path = "../crates/settings" } auto_update = { path = "../crates/auto_update" } person = { path = "../crates/person" } -relay_auth = { path = "../crates/relay_auth" } gpui.workspace = true gpui_platform.workspace = true diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 44b0970..ff147c6 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -81,9 +81,6 @@ fn main() { // Initialize person registry person::init(window, cx); - // Initialize relay auth registry - relay_auth::init(window, cx); - // Initialize device signer // // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md diff --git a/web/Cargo.toml b/web/Cargo.toml index a3cd061..48f09e4 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -15,7 +15,6 @@ device = { path = "../crates/device" } chat = { path = "../crates/chat" } settings = { path = "../crates/settings" } person = { path = "../crates/person" } -relay_auth = { path = "../crates/relay_auth" } gpui.workspace = true gpui_platform.workspace = true diff --git a/web/src/lib.rs b/web/src/lib.rs index 98b82cc..ca86a18 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -33,9 +33,6 @@ pub fn run() -> Result<(), JsValue> { // Initialize person registry person::init(window, cx); - // Initialize relay auth registry - relay_auth::init(window, cx); - // Initialize device signer // // NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md