From 2ecad5e273972ebe24c0ac34ae526c94561d4f81 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sun, 2 Aug 2026 11:57:26 +0700 Subject: [PATCH 1/2] support cli key --- Cargo.lock | 1 + crates/state/src/lib.rs | 18 +++++++++++------- desktop/Cargo.toml | 1 + desktop/src/main.rs | 30 +++++++++++++++++++++++++++++- web/src/lib.rs | 2 +- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 873b1c9..626fef1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,6 +1369,7 @@ dependencies = [ "gpui_platform", "gpui_windows", "log", + "nostr-sdk", "person", "reqwest_client", "settings", diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 9186a89..210356a 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -24,7 +24,7 @@ pub use nip4e::*; pub use nip05::*; pub use signer::{CoopAuthUrlHandler, UniversalSigner}; -pub fn init(window: &mut Window, cx: &mut App) { +pub fn init(window: &mut Window, cx: &mut App, cli_key: Option) { // 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`. @@ -37,7 +37,7 @@ pub fn init(window: &mut Window, cx: &mut App) { #[cfg(not(target_arch = "wasm32"))] gpui_tokio::init(cx); - NostrRegistry::set_global(cx.new(|cx| NostrRegistry::new(window, cx)), cx); + NostrRegistry::set_global(cx.new(|cx| NostrRegistry::new(window, cx, cli_key)), cx); } struct GlobalNostrRegistry(Entity); @@ -58,16 +58,16 @@ pub enum StateEvent { } impl StateEvent { + pub fn signer_changed(&self) -> bool { + matches!(self, StateEvent::SignerChanged) + } + pub fn error(error: T) -> Self where T: Into, { Self::Error(error.into()) } - - pub fn signer_changed(&self) -> bool { - matches!(self, StateEvent::SignerChanged) - } } /// Nostr Registry @@ -100,7 +100,7 @@ impl NostrRegistry { } /// Create a new nostr instance - fn new(window: &mut Window, cx: &mut Context) -> Self { + fn new(window: &mut Window, cx: &mut Context, cli_key: Option) -> Self { let signer = UniversalSigner::new(Keys::generate()); let authenticator = SignerAuthenticator::new(signer.clone()); @@ -133,6 +133,10 @@ impl NostrRegistry { if cfg!(target_arch = "wasm32") { cx.emit(StateEvent::NoSigner); + } else if let Some(secret) = cli_key { + // Use CLI-provided key -- same path as get_user_credential + let keys = Keys::new(secret); + this.set_signer(keys, cx); } else { this.get_user_credential(cx); } diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 76dfb1f..a1ff45a 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -48,3 +48,4 @@ reqwest_client.workspace = true log.workspace = true tracing-subscriber.workspace = true +nostr-sdk.workspace = true diff --git a/desktop/src/main.rs b/desktop/src/main.rs index 91db6a7..2c20e2d 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -7,6 +7,7 @@ use gpui::{ actions, point, px, size, }; use gpui_platform::application; +use nostr_sdk::prelude::SecretKey; use state::{APP_ID, CLIENT_NAME}; use ui::Root; @@ -16,6 +17,14 @@ fn main() { // Initialize logging tracing_subscriber::fmt::init(); + // Parse CLI arguments for --sec + let cli_key = parse_cli_key(); + if let Err(ref e) = cli_key { + eprintln!("Failed to parse --sec argument: {e}"); + std::process::exit(1); + } + let cli_key = cli_key.unwrap(); + // Run application application() .with_assets(Assets) @@ -75,7 +84,7 @@ fn main() { settings::init(window, cx); // Initialize the nostr client - state::init(window, cx); + state::init(window, cx, cli_key); // Initialize person registry person::init(window, cx); @@ -125,6 +134,25 @@ fn load_embedded_fonts(cx: &App) { .unwrap(); } +fn parse_cli_key() -> Result, String> { + let args: Vec = std::env::args().collect(); + let mut i = 0; + while i < args.len() { + if args[i] == "--sec" { + if i + 1 < args.len() { + let nsec = &args[i + 1]; + return SecretKey::parse(nsec) + .map(Some) + .map_err(|e| format!("Invalid nsec key '{nsec}': {e}")); + } else { + return Err("--sec requires a value (nsec1...)".to_string()); + } + } + i += 1; + } + Ok(None) +} + fn quit(_ev: &Quit, cx: &mut App) { log::info!("Gracefully quitting the application . . ."); cx.quit(); diff --git a/web/src/lib.rs b/web/src/lib.rs index d2a02a4..df97a5d 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -59,7 +59,7 @@ pub fn run() -> Result<(), JsValue> { settings::init(window, cx); // Initialize the nostr client - state::init(window, cx); + state::init(window, cx, None); // Initialize person registry person::init(window, cx); -- 2.49.1 From 677a42df2adb0274d1ac5c00aa71b2d9275a790b Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sun, 2 Aug 2026 14:08:42 +0700 Subject: [PATCH 2/2] fix get rooms --- Cargo.lock | 13 ---------- Cargo.toml | 2 +- crates/chat/src/lib.rs | 54 ++++++++++++++++-------------------------- 3 files changed, 21 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 626fef1..182111c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3989,15 +3989,6 @@ dependencies = [ "libc", ] -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -7478,14 +7469,10 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ - "matchers", "nu-ansi-term", - "once_cell", - "regex-automata", "sharded-slab", "smallvec", "thread_local", - "tracing", "tracing-core", "tracing-log", ] diff --git a/Cargo.toml b/Cargo.toml index 980975d..6d6016a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ schemars = "1" smallvec = "1.14.0" smol = "2" webbrowser = "1.0.4" -tracing-subscriber = { version = "0.3.18", features = ["fmt", "env-filter"] } +tracing-subscriber = { version = "0.3.18", features = ["fmt"] } errno = { version = "0.3.14", default-features = false } instant = "0.1" diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 30a7117..1003bae 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -627,13 +627,7 @@ impl ChatRegistry { /// Load all rooms from the database. pub fn get_rooms(&mut self, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - - let Some(public_key) = nostr.read(cx).current_user() else { - return; - }; - - let task = self.get_rooms_from_database(public_key, cx); + let task = self.get_rooms_task(cx); self.tasks.push(cx.spawn(async move |this, cx| { match task.await { @@ -655,61 +649,53 @@ impl ChatRegistry { } /// Create a task to load rooms from the database - fn get_rooms_from_database( - &self, - public_key: PublicKey, - cx: &App, - ) -> Task, Error>> { + fn get_rooms_task(&self, cx: &App) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); cx.background_spawn(async move { + let public_key = signer.get_public_key_async().await?; let contacts = client .database() .contacts_public_keys(public_key) .await .unwrap_or_default(); - // Query all cached rumor events (works with both old and new cache formats) let filter = Filter::new() .kind(Kind::ApplicationSpecificData) .custom_tag(SingleLetterTag::lowercase(Alphabet::K), "14"); - let events = client.database().query(filter).await?; - let mut rooms: HashSet = HashSet::new(); + let events = client.database().query(filter).await?; let mut grouped: HashMap> = HashMap::new(); for raw in events.into_iter() { if let Ok(rumor) = UnsignedEvent::from_json(&raw.content) - && rumor.tags.public_keys().peekable().peek().is_some() + && rumor.tags.public_keys().next().is_some() { + if rumor.pubkey != public_key + && !rumor.tags.public_keys().any(|k| k == public_key) + { + continue; + } grouped.entry(rumor.uniq_id()).or_default().push(rumor); } } - for (_id, mut messages) in grouped.into_iter() { - messages.sort_by_key(|m| Reverse(m.created_at)); + let mut rooms = HashSet::with_capacity(grouped.len()); - // Always use the latest message - let Some(latest) = messages.first() else { - continue; - }; + for (_id, messages) in grouped.into_iter() { + let latest = messages.iter().max_by_key(|m| m.created_at).unwrap(); + let room = Room::from(latest).organize(&public_key); - // Construct the room from the latest message. - // - // Call `.organize` to ensure the current user is at the end of the list. - let mut room = Room::from(latest).organize(&public_key); - - // Check if the user has responded to the room let user_sent = messages.iter().any(|m| m.pubkey == public_key); - - // Check if public keys are from the user's contacts let is_contact = room.members.iter().any(|k| contacts.contains(k)); - // Set the room's kind based on status - if user_sent || is_contact { - room = room.kind(RoomKind::Ongoing); - } + let room = if user_sent || is_contact { + room.kind(RoomKind::Ongoing) + } else { + room + }; rooms.insert(room); } -- 2.49.1