diff --git a/Cargo.lock b/Cargo.lock index eb12681..c4afe4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,12 @@ dependencies = [ "equator", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -530,6 +536,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -1349,6 +1364,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const-random" version = "0.1.18" @@ -1599,6 +1620,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1693,6 +1729,17 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1741,6 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -1845,6 +1893,12 @@ dependencies = [ "ui", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -1904,6 +1958,9 @@ name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] [[package]] name = "embed-resource" @@ -2008,7 +2065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2021,6 +2078,17 @@ dependencies = [ "svg_fmt", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "euclid" version = "0.22.13" @@ -2409,6 +2477,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.31" @@ -2811,6 +2890,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -2820,6 +2901,15 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.4.1" @@ -3538,6 +3628,17 @@ dependencies = [ "redox_syscall 0.7.0", ] +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linicon" version = "2.3.0" @@ -3982,7 +4083,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "aes", "base64", @@ -4007,7 +4108,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "async-utility", "futures-core", @@ -4020,7 +4121,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "btreecap", "flatbuffers", @@ -4032,27 +4133,26 @@ dependencies = [ [[package]] name = "nostr-gossip" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "nostr", ] [[package]] -name = "nostr-gossip-memory" +name = "nostr-gossip-sqlite" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ - "indexmap", - "lru", "nostr", "nostr-gossip", + "sqlx", "tokio", ] [[package]] name = "nostr-lmdb" version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "async-utility", "flume", @@ -4066,7 +4166,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" +source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7" dependencies = [ "async-utility", "async-wsocket", @@ -4097,7 +4197,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4591,6 +4691,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -4727,6 +4836,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -5007,7 +5137,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5421,6 +5551,26 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-embed" version = "8.11.0" @@ -5516,7 +5666,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6029,6 +6179,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -6089,6 +6249,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smol" @@ -6150,6 +6313,204 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener 5.4.1", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.114", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.114", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags 2.10.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags 2.10.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -6201,7 +6562,7 @@ dependencies = [ "gpui_tokio", "log", "nostr-connect", - "nostr-gossip-memory", + "nostr-gossip-sqlite", "nostr-lmdb", "nostr-sdk", "petname", @@ -6228,6 +6589,17 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -6546,7 +6918,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7753,7 +8125,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5d2a2da..251dbae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" } nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } -nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } +nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" } # Others anyhow = "1.0.44" diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 788547a..a08e146 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -98,7 +98,7 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); + let nip17_state = nostr.read(cx).nip17_state(); let device = DeviceRegistry::global(cx); let device_signer = device.read(cx).device_signer.clone(); @@ -114,8 +114,8 @@ impl ChatRegistry { subscriptions.push( // Observe the identity - cx.observe(&identity, |this, state, cx| { - if state.read(cx).messaging_relays_state() == RelayState::Set { + cx.observe(&nip17_state, |this, state, cx| { + if state.read(cx) == &RelayState::Configured { // Handle nostr notifications this.handle_notifications(cx); // Track unwrapping progress @@ -536,9 +536,9 @@ impl ChatRegistry { } // Set this room is ongoing if the new message is from current user - if author == nostr.read(cx).identity().read(cx).public_key() { - this.set_ongoing(cx); - } + // if author == nostr.read(cx).identity().read(cx).public_key() { + // this.set_ongoing(cx); + // } // Emit the new message to the room this.emit_message(message, cx); diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index ec55efc..bdea83e 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -216,28 +216,6 @@ impl Room { self.members.clone() } - /// Returns the members of the room with their messaging relays - pub fn members_with_relays(&self, cx: &App) -> Task)>> { - let nostr = NostrRegistry::global(cx); - let mut tasks = vec![]; - - for member in self.members.iter() { - let task = nostr.read(cx).messaging_relays(member, cx); - tasks.push((*member, task)); - } - - cx.background_spawn(async move { - let mut results = vec![]; - - for (public_key, task) in tasks.into_iter() { - let urls = task.await; - results.push((public_key, urls)); - } - - results - }) - } - /// Checks if the room has more than two members (group) pub fn is_group(&self) -> bool { self.members.len() > 2 @@ -266,17 +244,7 @@ impl Room { /// Display member is always different from the current user. pub fn display_member(&self, cx: &App) -> Person { let persons = PersonRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - - let target_member = self - .members - .iter() - .find(|&member| member != &public_key) - .or_else(|| self.members.first()) - .expect("Room should have at least one member"); - - persons.read(cx).get(target_member, cx) + persons.read(cx).get(&self.members[0], cx) } /// Merge the names of the first two members of the room. @@ -377,68 +345,79 @@ impl Room { }) } - /// Create a new message event (unsigned) - pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { + /// Create a new unsigned message event + pub fn create_message( + &self, + content: &str, + replies: Vec, + cx: &App, + ) -> Task> { let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); - // Get current user - let public_key = nostr.read(cx).identity().read(cx).public_key(); - - // Get room's subject let subject = self.subject.clone(); + let content = content.to_string(); - let mut tags = vec![]; + let mut member_and_relay_hints = HashMap::new(); - // Add receivers - // - // NOTE: current user will be removed from the list of receivers + // Populate the hashmap with member and relay hint tasks for member in self.members.iter() { - // Get relay hint if available - let relay_url = nostr.read(cx).relay_hint(member, cx); - - // Construct a public key tag with relay hint - let tag = TagStandard::PublicKey { - public_key: member.to_owned(), - relay_url, - alias: None, - uppercase: false, - }; - - tags.push(Tag::from_standardized_without_cell(tag)); + let hint = nostr.read(cx).relay_hint(member, cx); + member_and_relay_hints.insert(member.to_owned(), hint); } - // Add subject tag if it's present - if let Some(value) = subject { - tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( - value.to_string(), - ))); - } + cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; - // Add reply/quote tag - if replies.len() == 1 { - tags.push(Tag::event(replies[0])) - } else { - for id in replies { - let tag = TagStandard::Quote { - event_id: id.to_owned(), - relay_url: None, - public_key: None, + // List of event tags for each receiver + let mut tags = vec![]; + + for (member, task) in member_and_relay_hints.into_iter() { + // Skip current user + if member == public_key { + continue; + } + + // Get relay hint if available + let relay_url = task.await; + + // Construct a public key tag with relay hint + let tag = TagStandard::PublicKey { + public_key: member, + relay_url, + alias: None, + uppercase: false, }; - tags.push(Tag::from_standardized_without_cell(tag)) + + tags.push(Tag::from_standardized_without_cell(tag)); } - } - // Construct a direct message event - // - // WARNING: never sign and send this event to relays - let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) - .tags(tags) - .build(public_key); + // Add subject tag if present + if let Some(value) = subject { + tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( + value.to_string(), + ))); + } - // Ensure the event id has been generated - event.ensure_id(); + // Add all reply tags + for id in replies { + tags.push(Tag::event(id)) + } - event + // Construct a direct message event + // + // WARNING: never sign and send this event to relays + // TODO + let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) + .tags(tags) + .build(Keys::generate().public_key()); + + // Ensure the event ID has been generated + event.ensure_id(); + + Ok(event) + }) } /// Create a task to send a message to all room members @@ -450,46 +429,27 @@ impl Room { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - // Get current user's public key and relays - let current_user = nostr.read(cx).identity().read(cx).public_key(); - let current_user_relays = nostr.read(cx).messaging_relays(¤t_user, cx); - + let mut members = self.members(); let rumor = rumor.to_owned(); - // Get all members and their messaging relays - let task = self.members_with_relays(cx); - cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; - let current_user_relays = current_user_relays.await; - let mut members = task.await; + let current_user = signer.get_public_key().await?; // Remove the current user's public key from the list of receivers // the current user will be handled separately - members.retain(|(this, _)| this != ¤t_user); + members.retain(|this| this != ¤t_user); // Collect the send reports let mut reports: Vec = vec![]; - for (receiver, relays) in members.into_iter() { - // Check if there are any relays to send the message to - if relays.is_empty() { - reports.push(SendReport::new(receiver).relays_not_found()); - continue; - } - - // Ensure relay connection - for url in relays.iter() { - client.add_relay(url).await?; - client.connect_relay(url).await?; - } - + for receiver in members.into_iter() { // Construct the gift wrap event let event = EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?; // Send the gift wrap event to the messaging relays - match client.send_event(&event).to(&relays).await { + match client.send_event(&event).to_nip17().await { Ok(output) => { let id = output.id().to_owned(); let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); @@ -531,20 +491,8 @@ impl Room { // Only send a backup message to current user if sent successfully to others if reports.iter().all(|r| r.is_sent_success()) { - // Check if there are any relays to send the event to - if current_user_relays.is_empty() { - reports.push(SendReport::new(current_user).relays_not_found()); - return Ok(reports); - } - - // 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(current_user_relays, &event).await { + match client.send_event(&event).to_nip17().await { Ok(output) => { reports.push(SendReport::new(current_user).status(output)); } diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 791beee..a29ec1c 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; -use std::time::Duration; pub use actions::*; +use anyhow::Error; use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport}; use common::{nip96_upload, RenderedTimestamp}; use dock::panel::{Panel, PanelEvent}; @@ -244,27 +244,21 @@ impl ChatPanel { return; } - // Get the current room entity - let Some(room) = self.room.upgrade().map(|this| this.read(cx)) else { - return; - }; - // Get replies_to if it's present let replies: Vec = self.replies_to.read(cx).iter().copied().collect(); - // Create a temporary message for optimistic update - let rumor = room.create_message(&content, replies.as_ref(), cx); - let rumor_id = rumor.id.unwrap(); - - // Create a task for sending the message in the background - let send_message = room.send_message(&rumor, cx); + // Get a task to create temporary message for optimistic update + let Ok(get_rumor) = self + .room + .read_with(cx, |this, cx| this.create_message(&content, replies, cx)) + else { + return; + }; // Optimistically update message list - cx.spawn_in(window, async move |this, cx| { - // Wait for the delay - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; + let task: Task> = cx.spawn_in(window, async move |this, cx| { + let mut rumor = get_rumor.await?; + let rumor_id = rumor.id(); // Update the message list and reset the states this.update_in(cx, |this, window, cx| { @@ -280,43 +274,50 @@ impl ChatPanel { // Update the message list this.insert_message(&rumor, true, cx); - }) - .ok(); - }) - .detach(); - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - let result = send_message.await; + if let Ok(task) = this + .room + .read_with(cx, |this, cx| this.send_message(&rumor, cx)) + { + this.tasks.push(cx.spawn_in(window, async move |this, cx| { + let result = task.await; - this.update_in(cx, |this, window, cx| { - match result { - Ok(reports) => { - // Update room's status - this.room - .update(cx, |this, cx| { - if this.kind != RoomKind::Ongoing { - // Update the room kind to ongoing, - // but keep the room kind if send failed - if reports.iter().all(|r| !r.is_sent_success()) { - this.kind = RoomKind::Ongoing; - cx.notify(); - } + this.update_in(cx, |this, window, cx| { + match result { + Ok(reports) => { + // Update room's status + this.room + .update(cx, |this, cx| { + if this.kind != RoomKind::Ongoing { + // Update the room kind to ongoing, + // but keep the room kind if send failed + if reports.iter().all(|r| !r.is_sent_success()) { + this.kind = RoomKind::Ongoing; + cx.notify(); + } + } + }) + .ok(); + + // Insert the sent reports + this.reports_by_id.insert(rumor_id, reports); + + cx.notify(); } - }) - .ok(); - - // Insert the sent reports - this.reports_by_id.insert(rumor_id, reports); - - cx.notify(); - } - Err(e) => { - window.push_notification(e.to_string(), cx); - } + Err(e) => { + window.push_notification(e.to_string(), cx); + } + } + }) + .ok(); + })) } - }) - .ok(); - })); + })?; + + Ok(()) + }); + + task.detach(); } /// Insert a message into the chat panel diff --git a/crates/coop/src/command_bar.rs b/crates/coop/src/command_bar.rs index dbf4532..6cea72f 100644 --- a/crates/coop/src/command_bar.rs +++ b/crates/coop/src/command_bar.rs @@ -169,7 +169,6 @@ impl CommandBar { fn search(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); let query = self.find_input.read(cx).value(); // Return if the query is empty @@ -191,7 +190,7 @@ impl CommandBar { // Block the input until the search completes self.set_finding(true, window, cx); - let find_users = if identity.read(cx).owned { + let find_users = if nostr.read(cx).owned_signer() { nostr.read(cx).wot_search(&query, cx) } else { nostr.read(cx).search(&query, cx) @@ -245,17 +244,28 @@ impl CommandBar { fn create(&mut self, window: &mut Window, cx: &mut Context) { let chat = ChatRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); + let async_chat = chat.downgrade(); + let nostr = NostrRegistry::global(cx); + let signer_pkey = nostr.read(cx).signer_pkey(cx); + + // Get all selected public keys let receivers = self.selected(cx); - chat.update(cx, |this, cx| { - let room = cx.new(|_| Room::new(public_key, receivers)); - this.emit_room(room.downgrade(), cx); + let task: Task> = cx.spawn_in(window, async move |_this, cx| { + let public_key = signer_pkey.await?; + + async_chat.update_in(cx, |this, window, cx| { + let room = cx.new(|_| Room::new(public_key, receivers)); + this.emit_room(room.downgrade(), cx); + + window.close_modal(cx); + })?; + + Ok(()) }); - window.close_modal(cx); + task.detach(); } fn select(&mut self, pkey: PublicKey, cx: &mut Context) { diff --git a/crates/coop/src/panels/greeter.rs b/crates/coop/src/panels/greeter.rs index 04541c9..9b58edd 100644 --- a/crates/coop/src/panels/greeter.rs +++ b/crates/coop/src/panels/greeter.rs @@ -29,6 +29,26 @@ impl GreeterPanel { focus_handle: cx.focus_handle(), } } + + fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { + let nostr = NostrRegistry::global(cx); + let signer_pkey = nostr.read(cx).signer_pkey(cx); + + cx.spawn_in(window, async move |_this, cx| { + if let Ok(public_key) = signer_pkey.await { + cx.update(|window, cx| { + Workspace::add_panel( + profile::init(public_key, window, cx), + DockPlacement::Center, + window, + cx, + ); + }) + .ok(); + } + }) + .detach(); + } } impl Panel for GreeterPanel { @@ -62,12 +82,11 @@ impl Render for GreeterPanel { const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); + let nip65_state = nostr.read(cx).nip65_state(); + let nip17_state = nostr.read(cx).nip17_state(); - let relay_list_state = identity.read(cx).relay_list_state(); - let messaging_relay_state = identity.read(cx).messaging_relays_state(); - let required_actions = - relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet; + let required_actions = nip65_state.read(cx) == &RelayState::NotConfigured + || nip17_state.read(cx) == &RelayState::NotConfigured; h_flex() .size_full() @@ -128,7 +147,7 @@ impl Render for GreeterPanel { v_flex() .gap_2() .w_full() - .when(relay_list_state == RelayState::NotSet, |this| { + .when(nip65_state.read(cx).not_configured(), |this| { this.child( Button::new("relaylist") .icon(Icon::new(IconName::Relay)) @@ -146,31 +165,28 @@ impl Render for GreeterPanel { }), ) }) - .when( - messaging_relay_state == RelayState::NotSet, - |this| { - this.child( - Button::new("import") - .icon(Icon::new(IconName::Relay)) - .label("Set up messaging relays") - .ghost() - .small() - .no_center() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - messaging_relays::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), - ) - }, - ), + .when(nip17_state.read(cx).not_configured(), |this| { + this.child( + Button::new("import") + .icon(Icon::new(IconName::Relay)) + .label("Set up messaging relays") + .ghost() + .small() + .no_center() + .on_click(move |_ev, window, cx| { + Workspace::add_panel( + messaging_relays::init(window, cx), + DockPlacement::Center, + window, + cx, + ); + }), + ) + }), ), ) }) - .when(!identity.read(cx).owned, |this| { + .when(!nostr.read(cx).owned_signer(), |this| { this.child( v_flex() .gap_2() @@ -257,14 +273,9 @@ impl Render for GreeterPanel { .ghost() .small() .no_center() - .on_click(move |_ev, window, cx| { - Workspace::add_panel( - profile::init(window, cx), - DockPlacement::Center, - window, - cx, - ); - }), + .on_click(cx.listener(move |this, _ev, window, cx| { + this.add_profile_panel(window, cx) + })), ) .child( Button::new("invite") diff --git a/crates/coop/src/panels/messaging_relays.rs b/crates/coop/src/panels/messaging_relays.rs index 6388d7c..0625dd0 100644 --- a/crates/coop/src/panels/messaging_relays.rs +++ b/crates/coop/src/panels/messaging_relays.rs @@ -156,29 +156,20 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let relays = self.relays.clone(); + + let tags: Vec = self + .relays + .iter() + .map(|relay| Tag::relay(relay.clone())) + .collect(); let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - - let tags: Vec = relays - .iter() - .map(|relay| Tag::relay(relay.clone())) - .collect(); - + // Construct nip17 event builder let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); let event = client.sign_event_builder(builder).await?; // Set messaging relays - client.send_event(&event).to(urls).await?; - - // Connect to messaging relays - for relay in relays.iter() { - client.add_relay(relay).await.ok(); - client.connect_relay(relay).await.ok(); - } + client.send_event(&event).to_nip65().await?; Ok(()) }); diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs index 8d48095..ad806db 100644 --- a/crates/coop/src/panels/profile.rs +++ b/crates/coop/src/panels/profile.rs @@ -22,8 +22,8 @@ use ui::input::{InputState, TextInput}; use ui::notification::Notification; use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| ProfilePanel::new(window, cx)) +pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| ProfilePanel::new(public_key, window, cx)) } #[derive(Debug)] @@ -31,6 +31,9 @@ pub struct ProfilePanel { name: SharedString, focus_handle: FocusHandle, + /// User's public key + public_key: PublicKey, + /// User's name text input name_input: Entity, @@ -51,13 +54,10 @@ pub struct ProfilePanel { } impl ProfilePanel { - fn new(window: &mut Window, cx: &mut Context) -> Self { + fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context) -> Self { let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice")); let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me")); - - // Hidden input for avatar url let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg")); - // Use multi-line input for bio let bio_input = cx.new(|cx| { InputState::new(window, cx) @@ -66,13 +66,10 @@ impl ProfilePanel { .placeholder("A short introduce about you.") }); + // Get user's profile and update inputs cx.defer_in(window, move |this, window, cx| { - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&public_key, cx); - // Set all input's values with current profile this.set_profile(profile, window, cx); }); @@ -80,6 +77,7 @@ impl ProfilePanel { Self { name: "Update Profile".into(), focus_handle: cx.focus_handle(), + public_key, name_input, avatar_input, bio_input, @@ -209,7 +207,7 @@ impl ProfilePanel { fn set_metadata(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); + let public_key = self.public_key; // Get the old metadata let persons = PersonRegistry::global(cx); @@ -289,9 +287,7 @@ impl Focusable for ProfilePanel { impl Render for ProfilePanel { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { - let nostr = NostrRegistry::global(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let shorten_pkey = SharedString::from(shorten_pubkey(public_key, 8)); + let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8)); // Get the avatar let avatar_input = self.avatar_input.read(cx).value(); @@ -390,7 +386,7 @@ impl Render for ProfilePanel { .ghost() .on_click(cx.listener(move |this, _ev, window, cx| { this.copy( - public_key.to_bech32().unwrap(), + this.public_key.to_bech32().unwrap(), window, cx, ); diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 2d3cc0b..4d32e2b 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -9,10 +9,11 @@ use gpui::{ div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, }; +use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::NostrRegistry; -use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; +use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; use titlebar::TitleBar; use ui::avatar::Avatar; use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension}; @@ -35,6 +36,9 @@ pub struct Workspace { /// App's Command Bar command_bar: Entity, + /// Current User + current_user: Entity>, + /// Event subscriptions _subscriptions: SmallVec<[Subscription; 3]>, } @@ -42,20 +46,23 @@ pub struct Workspace { impl Workspace { fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); + let current_user = cx.new(|_| None); + + let nostr = NostrRegistry::global(cx); + let nip65_state = nostr.read(cx).nip65_state(); + + // Titlebar let titlebar = cx.new(|_| TitleBar::new()); + + // Command bar let command_bar = cx.new(|cx| CommandBar::new(window, cx)); + + // Dock let dock = cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); let mut subscriptions = smallvec![]; - subscriptions.push( - // Automatically sync theme with system appearance - window.observe_window_appearance(|window, cx| { - Theme::sync_system_appearance(Some(window), cx); - }), - ); - subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { @@ -100,6 +107,15 @@ impl Workspace { }), ); + subscriptions.push( + // Observe the NIP-65 state + cx.observe(&nip65_state, move |this, state, cx| { + if state.read(cx).idle() { + this.get_current_user(cx); + } + }), + ); + // Set the default layout for app's dock cx.defer_in(window, |this, window, cx| { this.set_layout(window, cx); @@ -109,6 +125,7 @@ impl Workspace { titlebar, dock, command_bar, + current_user, _subscriptions: subscriptions, } } @@ -173,18 +190,35 @@ impl Workspace { }); } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { + fn get_current_user(&self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let identity = nostr.read(cx).identity(); + let client = nostr.read(cx).client(); + let current_user = self.current_user.downgrade(); + cx.spawn(async move |_this, cx| { + if let Some(signer) = client.signer() { + if let Ok(public_key) = signer.get_public_key().await { + current_user + .update(cx, |this, cx| { + *this = Some(public_key); + cx.notify(); + }) + .ok(); + } + } + }) + .detach(); + } + + fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { h_flex() .h(TITLEBAR_HEIGHT) .flex_1() .justify_between() .gap_2() - .when_some(identity.read(cx).public_key, |this, public_key| { + .when_some(self.current_user.read(cx).as_ref(), |this, public_key| { let persons = PersonRegistry::global(cx); - let profile = persons.read(cx).get(&public_key, cx); + let profile = persons.read(cx).get(public_key, cx); this.child( h_flex() diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index e4d2ce8..d94ac41 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -40,7 +40,7 @@ pub struct DeviceRegistry { tasks: Vec>>, /// Subscriptions - _subscriptions: SmallVec<[Subscription; 1]>, + _subscriptions: SmallVec<[Subscription; 2]>, } impl DeviceRegistry { @@ -58,7 +58,8 @@ impl DeviceRegistry { fn new(cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let identity = nostr.read(cx).identity(); + let nip65_state = nostr.read(cx).nip65_state(); + let nip17_state = nostr.read(cx).nip17_state(); let device_signer = cx.new(|_| None); let requests = cx.new(|_| HashSet::default()); @@ -70,21 +71,26 @@ impl DeviceRegistry { let mut tasks = vec![]; subscriptions.push( - // Observe the identity entity - cx.observe(&identity, |this, state, cx| { - match state.read(cx).relay_list_state() { - RelayState::Initial => { + // Observe the NIP-65 state + cx.observe(&nip65_state, |this, state, cx| { + match state.read(cx) { + RelayState::Idle => { this.reset(cx); } - RelayState::Set => { + RelayState::Configured => { this.get_announcement(cx); - - if state.read(cx).messaging_relays_state() == RelayState::Set { - this.get_messages(cx); - } } _ => {} - } + }; + }), + ); + + subscriptions.push( + // Observe the NIP-17 state + cx.observe(&nip17_state, |this, state, cx| { + if state.read(cx) == &RelayState::Configured { + this.get_messages(cx); + }; }), ); @@ -265,29 +271,26 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let device_signer = self.device_signer.read(cx).clone(); + let messaging_relays = nostr.read(cx).messaging_relays(cx); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx); - - cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { let urls = messaging_relays.await; + let user_signer = client.signer().context("Signer not found")?; + let public_key = user_signer.get_public_key().await?; // Get messages with dekey if let Some(signer) = device_signer.as_ref() { - if let Ok(pkey) = signer.get_public_key().await { - let filter = Filter::new().kind(Kind::GiftWrap).pubkey(pkey); - let id = SubscriptionId::new(DEVICE_GIFTWRAP); + let device_pkey = signer.get_public_key().await?; + let filter = Filter::new().kind(Kind::GiftWrap).pubkey(device_pkey); + let id = SubscriptionId::new(DEVICE_GIFTWRAP); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); + // Construct target for subscription + let target = urls + .iter() + .map(|relay| (relay, vec![filter.clone()])) + .collect::>(); - if let Err(e) = client.subscribe(target).with_id(id).await { - log::error!("Failed to subscribe to gift wrap events: {e}"); - } - } + client.subscribe(target).with_id(id).await?; } // Get messages with user key @@ -300,11 +303,12 @@ impl DeviceRegistry { .map(|relay| (relay, vec![filter.clone()])) .collect::>(); - if let Err(e) = client.subscribe(target).with_id(id).await { - log::error!("Failed to subscribe to gift wrap events: {e}"); - } - }) - .detach(); + client.subscribe(target).with_id(id).await?; + + Ok(()) + }); + + task.detach(); } /// Get device announcement for current user @@ -312,11 +316,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct the filter for the device announcement event let filter = Filter::new() @@ -324,14 +326,8 @@ impl DeviceRegistry { .author(public_key) .limit(1); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - let mut stream = client - .stream_events(target) + .stream_events(filter) .timeout(Duration::from_secs(TIMEOUT)) .await?; @@ -373,16 +369,12 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - + // Generate a new device keys let keys = Keys::generate(); let secret = keys.secret_key().to_secret_hex(); let n = keys.public_key(); let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - // Construct an announcement event let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ Tag::custom(TagKind::custom("n"), vec![n]), @@ -391,7 +383,7 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Publish announcement - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; // Save device keys to the database Self::set_keys(&client, &secret).await?; @@ -459,11 +451,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct a filter for device key requests let filter = Filter::new() @@ -471,14 +461,8 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - // Subscribe to the device key requests on user's write relays - client.subscribe(target).await?; + client.subscribe(filter).await?; Ok(()) }); @@ -491,11 +475,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct a filter for device key requests let filter = Filter::new() @@ -503,14 +485,8 @@ impl DeviceRegistry { .author(public_key) .since(Timestamp::now()); - // Construct target for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - // Subscribe to the device key requests on user's write relays - client.subscribe(target).await?; + client.subscribe(filter).await?; Ok(()) }); @@ -523,9 +499,6 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let app_keys = nostr.read(cx).app_keys().clone(); let app_pubkey = app_keys.public_key(); @@ -557,8 +530,6 @@ impl DeviceRegistry { Ok(Some(keys)) } None => { - let urls = write_relays.await; - // Construct an event for device key request let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ Tag::client(app_name()), @@ -567,7 +538,7 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Send the event to write relays - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; Ok(None) } @@ -640,11 +611,7 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); - let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; let signer = client.signer().context("Signer not found")?; // Get device keys @@ -673,7 +640,7 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Send the response event to the user's relay list - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; Ok(()) }); diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 6a88e0c..1f182ea 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -138,16 +138,36 @@ impl RelayAuth { let mut notifications = client.notifications(); while let Some(notification) = notifications.next().await { - if let ClientNotification::Message { - message: RelayMessage::Auth { challenge }, - relay_url, - } = notification - { - let request = AuthRequest::new(challenge, relay_url); + match notification { + ClientNotification::Message { relay_url, message } => { + match message { + RelayMessage::Auth { challenge } => { + let request = AuthRequest::new(challenge, relay_url); - if let Err(e) = tx.send_async(request).await { - log::error!("Failed to send auth request: {}", e); + if let Err(e) = tx.send_async(request).await { + log::error!("Failed to send auth request: {}", e); + } + } + RelayMessage::Ok { + event_id, message, .. + } => { + let msg = MachineReadablePrefix::parse(&message); + let mut tracker = tracker().write().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) + } + } + _ => {} + } } + ClientNotification::Shutdown => break, + _ => {} } } } diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 9781b8d..42645c5 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; @@ -121,7 +121,7 @@ pub struct AppSettings { _subscriptions: SmallVec<[Subscription; 1]>, /// Background tasks - _tasks: SmallVec<[Task<()>; 1]>, + tasks: SmallVec<[Task<()>; 1]>, } impl AppSettings { @@ -163,8 +163,8 @@ impl AppSettings { Self { values: Settings::default(), + tasks, _subscriptions: subscriptions, - _tasks: tasks, } } @@ -172,7 +172,6 @@ impl AppSettings { fn get_from_database(cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let current_user = nostr.read(cx).identity().read(cx).public_key; cx.background_spawn(async move { // Construct a filter to get the latest settings @@ -181,9 +180,12 @@ impl AppSettings { .identifier(SETTINGS_IDENTIFIER) .limit(1); - if let Some(public_key) = current_user { - // Push author to the filter - filter = filter.author(public_key); + // If the signer is available, get settings belonging to the current user + if let Some(signer) = client.signer() { + if let Ok(public_key) = signer.get_public_key().await { + // Push author to the filter + filter = filter.author(public_key); + } } if let Some(event) = client.database().query(filter).await?.first_owned() { @@ -198,7 +200,7 @@ impl AppSettings { pub fn load(&mut self, cx: &mut Context) { let task = Self::get_from_database(cx); - self._tasks.push( + self.tasks.push( // Run task in the background cx.spawn(async move |this, cx| { if let Ok(settings) = task.await { @@ -216,10 +218,12 @@ impl AppSettings { pub fn save(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let public_key = nostr.read(cx).identity().read(cx).public_key(); if let Ok(content) = serde_json::to_string(&self.values) { let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tag(Tag::identifier(SETTINGS_IDENTIFIER)) .build(public_key) diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 2d04fc2..bc3b846 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -10,7 +10,7 @@ common = { path = "../common" } nostr-sdk.workspace = true nostr-lmdb.workspace = true nostr-connect.workspace = true -nostr-gossip-memory.workspace = true +nostr-gossip-sqlite.workspace = true gpui.workspace = true gpui_tokio.workspace = true diff --git a/crates/state/src/identity.rs b/crates/state/src/identity.rs deleted file mode 100644 index 31bb690..0000000 --- a/crates/state/src/identity.rs +++ /dev/null @@ -1,101 +0,0 @@ -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) - } -} - -/// Identity -#[derive(Debug, Default)] -pub struct Identity { - /// Signer's public key - pub public_key: Option, - - /// Whether the identity is owned by the user - pub owned: bool, - - /// 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, - owned: true, - relay_list: RelayState::default(), - messaging_relays: RelayState::default(), - } - } - - /// Resets the relay states to their default values. - pub fn reset_relay_state(&mut self) { - self.relay_list = RelayState::default(); - self.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 - } - - /// Sets the state of the NIP-17 relays. - 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 - } - - /// Force getting the public key of the identity. - /// - /// Panics if the public key is not set. - pub fn public_key(&self) -> PublicKey { - self.public_key.unwrap() - } - - /// 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; - } - - /// Sets whether the identity is owned by the user. - pub fn set_owned(&mut self, owned: bool) { - self.owned = owned; - } -} diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 5fb7df6..cd8830d 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,31 +1,29 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::os::unix::fs::PermissionsExt; use std::sync::Arc; use std::time::Duration; -use anyhow::{anyhow, Error}; +use anyhow::{anyhow, Context as AnyhowContext, Error}; use common::{config_dir, CLIENT_NAME}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; +use gpui_tokio::Tokio; use nostr_connect::prelude::*; +use nostr_gossip_sqlite::prelude::*; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; mod device; mod event; mod gossip; -mod identity; mod nip05; mod signer; pub use device::*; pub use event::*; pub use gossip::*; -pub use identity::*; pub use nip05::*; pub use signer::*; -use crate::identity::Identity; - /// Default timeout for subscription pub const TIMEOUT: u64 = 3; /// Default delay for searching @@ -55,9 +53,19 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [ ]; pub fn init(cx: &mut App) { + // 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(); + // Initialize the tokio runtime gpui_tokio::init(cx); + // Initialize the event tracker + let _tracker = tracker(); + NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); } @@ -74,23 +82,26 @@ pub struct NostrRegistry { /// Nostr signer signer: Arc, + /// By default, Coop generates a new signer for new users. + /// + /// This flag indicates whether the signer is user-owned or Coop-generated. + owned_signer: bool, + + /// NIP-65 relay state + nip65: Entity, + + /// NIP-17 relay state + nip17: Entity, + /// App keys /// /// Used for Nostr Connect and NIP-4e operations app_keys: Keys, - /// Current identity (user's public key) - /// - /// Set by the current Nostr signer - identity: Entity, - - /// Gossip implementation - gossip: Entity, - /// Tasks for asynchronous operations tasks: Vec>>, - /// Subscriptions + /// Event subscriptions _subscriptions: Vec, } @@ -107,27 +118,39 @@ impl NostrRegistry { /// 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 lmdb + // Construct the nostr lmdb instance let lmdb = cx.foreground_executor().block_on(async move { NostrLmdb::open(config_dir().join("nostr")) .await .expect("Failed to initialize database") }); + // Use tokio to spawn a task to build the gossip instance + let build_gossip_sqlite = Tokio::spawn(cx, async move { + NostrGossipSqlite::open(config_dir().join("gossip")) + .await + .expect("Failed to initialize gossip") + }); + + // Initialize the nostr gossip instance + let gossip = cx.foreground_executor().block_on(async move { + build_gossip_sqlite + .await + .expect("Failed to initialize gossip") + }); + // Construct the nostr signer - let keys = Keys::generate(); - let signer = Arc::new(CoopSigner::new(keys)); + let app_keys = Self::create_or_init_app_keys().unwrap_or(Keys::generate()); + let signer = Arc::new(CoopSigner::new(app_keys.clone())); + + // Construct the relay states entity + let nip65 = cx.new(|_| RelayState::default()); + let nip17 = cx.new(|_| RelayState::default()); // Construct the nostr client let client = ClientBuilder::default() .signer(signer.clone()) + .gossip(gossip) .database(lmdb) .automatic_authentication(false) .verify_subscriptions(false) @@ -136,83 +159,23 @@ impl NostrRegistry { }) .build(); - // Construct the event tracker - let _tracker = tracker(); - - // Get the app keys - let app_keys = Self::create_or_init_app_keys().unwrap(); - - // Construct the gossip entity - let gossip = cx.new(|_| Gossip::default()); - let async_gossip = gossip.downgrade(); - - // Construct the identity entity - let identity = cx.new(|_| Identity::new()); - - // 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| { - if state.read(cx).has_public_key() { - match state.read(cx).relay_list_state() { - RelayState::Initial => { - this.get_relay_list(cx); - } - RelayState::Set => { - if state.read(cx).messaging_relays_state() == RelayState::Initial { - this.get_profile(cx); - this.get_messaging_relays(cx); - }; - } - _ => {} - } + // Observe the NIP-65 state + cx.observe(&nip65, |this, state, cx| { + if state.read(cx).configured() { + this.get_profile(cx); + this.get_messaging_relays(cx); } }), ); - 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(()) - }), - ); - cx.defer(|cx| { let nostr = NostrRegistry::global(cx); nostr.update(cx, |this, cx| { + this.connect(cx); this.get_identity(cx); }); }); @@ -220,89 +183,188 @@ impl NostrRegistry { Self { client, signer, + owned_signer: false, + nip65, + nip17, app_keys, - identity, - gossip, + tasks: vec![], _subscriptions: subscriptions, - tasks, } } - /// Handle nostr notifications - async fn handle_notifications(client: &Client, tx: &flume::Sender) -> Result<(), Error> { - // Add bootstrap relay to the relay pool - for url in BOOTSTRAP_RELAYS.into_iter() { - client.add_relay(url).await?; - } + /// Connect to all bootstrap relays + fn connect(&mut self, cx: &mut Context) { + let client = self.client(); - // Add search relay to the relay pool - for url in SEARCH_RELAYS.into_iter() { - client.add_relay(url).await?; - } + self.tasks.push(cx.background_spawn(async move { + // Add bootstrap relay to the relay pool + for url in BOOTSTRAP_RELAYS.into_iter() { + client.add_relay(url).await?; + } - // Add wot relay to the relay pool - for url in WOT_RELAYS.into_iter() { - client.add_relay(url).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; + // Add wot relay to the relay pool + for url in WOT_RELAYS.into_iter() { + client.add_relay(url).await?; + } - // Handle nostr notifications - let mut notifications = client.notifications(); - let mut processed_events = HashSet::new(); + // Connect to all added relays + client.connect().await; - while let Some(notification) = notifications.next().await { - if let ClientNotification::Message { message, relay_url } = notification { - match message { - RelayMessage::Event { - event, - subscription_id, - } => { - if !processed_events.insert(event.id) { - // Skip if the event has already been processed - continue; - } + Ok(()) + })); + } - match event.kind { - Kind::RelayList => { - // Automatically get messaging relays for each member when the user opens a room - if subscription_id.as_str().starts_with("room-") { - Self::get_adv_events_by(client, event.as_ref()).await?; - } + /// Get the nostr client + pub fn client(&self) -> Client { + self.client.clone() + } - 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; + /// Get the app keys + pub fn app_keys(&self) -> &Keys { + &self.app_keys + } - // 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) - } - } - _ => {} + /// Returns whether the current signer is owned by user + pub fn owned_signer(&self) -> bool { + self.owned_signer + } + + /// Set whether the current signer is owned by user + pub fn set_owned_signer(&mut self, owned: bool, cx: &mut Context) { + self.owned_signer = owned; + cx.notify(); + } + + /// Get the NIP-65 state + pub fn nip65_state(&self) -> Entity { + self.nip65.clone() + } + + /// Get the NIP-17 state + pub fn nip17_state(&self) -> Entity { + self.nip17.clone() + } + + /// Get current signer's public key + pub fn signer_pkey(&self, cx: &App) -> Task> { + let client = self.client(); + + cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + + Ok(public_key) + }) + } + + /// Get a relay hint (messaging relay) for a given public key + /// + /// Used for building chat messages + pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Task> { + let client = self.client(); + let public_key = public_key.to_owned(); + + cx.background_spawn(async move { + let filter = Filter::new() + .author(public_key) + .kind(Kind::InboxRelays) + .limit(1); + + if let Ok(events) = client.database().query(filter).await { + if let Some(event) = events.first_owned() { + let relays: Vec = nip17::extract_owned_relay_list(event).collect(); + return relays.first().cloned(); } } - } - Ok(()) + None + }) } - /// Automatically get messaging relays and encryption announcement from a received relay list + /// Get a list of messaging relays with current signer's public key + pub fn messaging_relays(&self, cx: &App) -> Task> { + let client = self.client(); + + cx.background_spawn(async move { + let Ok(signer) = client.signer().context("Signer not found") else { + return vec![]; + }; + + let Ok(public_key) = signer.get_public_key().await else { + return vec![]; + }; + + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + client + .database() + .query(filter) + .await + .ok() + .and_then(|events| events.first_owned()) + .map(|event| nip17::extract_owned_relay_list(event).collect()) + .unwrap_or_default() + }) + } + + /// Reset all relay states + pub fn reset_relay_states(&mut self, cx: &mut Context) { + self.nip65.update(cx, |this, cx| { + *this = RelayState::default(); + cx.notify(); + }); + self.nip17.update(cx, |this, cx| { + *this = RelayState::default(); + cx.notify(); + }); + } + + /// Set the signer for the nostr client and verify the public key + pub fn set_signer(&mut self, new: T, owned: bool, cx: &mut Context) + where + T: NostrSigner + 'static, + { + let client = self.client(); + let signer = self.signer.clone(); + + // Create a task to update the signer and verify the public key + let task: Task> = cx.background_spawn(async move { + // Update signer + signer.switch(new).await; + + // Verify signer + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + + log::info!("Signer's public key: {public_key}"); + + Ok(()) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + // set signer + task.await?; + + // Update states + this.update(cx, |this, cx| { + this.reset_relay_states(cx); + this.get_relay_list(cx); + this.set_owned_signer(owned, cx); + })?; + + Ok(()) + })); + } + + /* async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> { // Subscription options let opts = SubscribeAutoCloseOptions::default() @@ -348,6 +410,7 @@ impl NostrRegistry { Ok(()) } + */ /// Get or create a new app keys fn create_or_init_app_keys() -> Result { @@ -377,124 +440,15 @@ impl NostrRegistry { Ok(keys) } - /// Get the nostr client - pub fn client(&self) -> Client { - self.client.clone() - } - - /// Get the app keys - pub fn app_keys(&self) -> &Keys { - &self.app_keys - } - - /// Get current identity - pub fn identity(&self) -> Entity { - self.identity.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 write relays for a given public key - pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - let client = self.client(); - let relays = self.gossip.read(cx).write_relays(public_key); - - cx.background_spawn(async move { - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relays - }) - } - - /// Get a list of read relays for a given public key - pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - let client = self.client(); - let relays = self.gossip.read(cx).read_relays(public_key); - - cx.background_spawn(async move { - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relays - }) - } - - /// Get a list of messaging relays for a given public key - pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task> { - let client = self.client(); - let relays = self.gossip.read(cx).messaging_relays(public_key); - - cx.background_spawn(async move { - // Ensure relay connections - for url in relays.iter() { - client.add_relay(url).await.ok(); - client.connect_relay(url).await.ok(); - } - - relays - }) - } - - /// Set the signer for the nostr client and verify the public key - pub fn set_signer(&mut self, new: T, owned: bool, cx: &mut Context) - where - T: NostrSigner + 'static, - { - let identity = self.identity.downgrade(); - let signer = self.signer.clone(); - - // Create a task to update the signer and verify the public key - let task: Task> = cx.background_spawn(async move { - // Update signer - signer.switch(new).await; - - // Verify signer - let public_key = signer.get_public_key().await?; - log::info!("test: {public_key:?}"); - - 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); - this.reset_relay_state(); - this.set_owned(owned); - cx.notify(); - })?; - } - Err(e) => { - log::error!("Failed to set signer: {e}"); - } - }; - - 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().read(cx).public_key(); + let nip65 = self.nip65.downgrade(); let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let filter = Filter::new() .kind(Kind::RelayList) .author(public_key) @@ -531,7 +485,7 @@ impl NostrRegistry { // Subscribe to the relay list events client.subscribe(target).await?; - return Ok(RelayState::Set); + return Ok(RelayState::Configured); } Err(e) => { log::error!("Failed to receive relay list event: {e}"); @@ -539,15 +493,15 @@ impl NostrRegistry { } } - Ok(RelayState::NotSet) + Ok(RelayState::NotConfigured) }); self.tasks.push(cx.spawn(async move |_this, cx| { match task.await { - Ok(state) => { - async_identity + Ok(new_state) => { + nip65 .update(cx, |this, cx| { - this.set_relay_list_state(state); + *this = new_state; cx.notify(); }) .ok(); @@ -561,19 +515,78 @@ impl NostrRegistry { })); } + /// Get messaging relays for current user + fn get_messaging_relays(&mut self, cx: &mut Context) { + let client = self.client(); + let nip17 = self.nip17.downgrade(); + + let task: Task> = cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + + // Construct the filter for inbox relays + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + // Stream events from the write relays + let mut stream = client + .stream_events(filter) + .timeout(Duration::from_secs(TIMEOUT)) + .await?; + + while let Some((_url, res)) = stream.next().await { + match res { + Ok(event) => { + log::info!("Received messaging relays event: {event:?}"); + + // Construct a filter to continuously receive relay list events + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .since(Timestamp::now()); + + // Subscribe to the relay list events + client.subscribe(filter).await?; + + return Ok(RelayState::Configured); + } + Err(e) => { + log::error!("Failed to get messaging relays: {e}"); + } + } + } + + Ok(RelayState::NotConfigured) + }); + + self.tasks.push(cx.spawn(async move |_this, cx| { + match task.await { + Ok(new_state) => { + nip17 + .update(cx, |this, cx| { + *this = new_state; + cx.notify(); + }) + .ok(); + } + Err(e) => { + log::error!("Failed to get messaging relays: {e}"); + } + } + + Ok(()) + })); + } + /// Get profile and contact list for current user fn get_profile(&mut self, cx: &mut Context) { let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); let task: Task> = cx.background_spawn(async move { - let mut urls = write_relays.await; - urls.extend( - BOOTSTRAP_RELAYS - .iter() - .filter_map(|url| RelayUrl::parse(url).ok()), - ); + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; // Construct subscription options let opts = SubscribeAutoCloseOptions::default() @@ -592,13 +605,10 @@ impl NostrRegistry { .limit(1) .author(public_key); - // Construct targets for subscription - let target = urls - .into_iter() - .map(|relay| (relay, vec![metadata.clone(), contact_list.clone()])) - .collect::>(); - - client.subscribe(target).close_on(opts).await?; + client + .subscribe(vec![metadata, contact_list]) + .close_on(opts) + .await?; Ok(()) }); @@ -606,90 +616,14 @@ impl NostrRegistry { task.detach(); } - /// 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().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); - - let task: Task> = cx.background_spawn(async move { - let urls = write_relays.await; - - // Construct the filter for inbox relays - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - // Construct targets for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - - // Stream events from the write relays - let mut stream = client - .stream_events(target) - .timeout(Duration::from_secs(TIMEOUT)) - .await?; - - while let Some((_url, res)) = stream.next().await { - match res { - Ok(event) => { - log::info!("Received messaging relays event: {event:?}"); - - // Construct a filter to continuously receive relay list events - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .since(Timestamp::now()); - - // Construct targets for subscription - let target = urls - .iter() - .map(|relay| (relay, vec![filter.clone()])) - .collect::>(); - - // Subscribe to the relay list events - client.subscribe(target).await?; - - return Ok(RelayState::Set); - } - Err(e) => { - log::error!("Failed to get messaging relays: {e}"); - } - } - } - - 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(()) - })); - } - /// Get contact list for the current user pub fn get_contact_list(&self, cx: &App) -> Task, Error>> { let client = self.client(); - let public_key = self.identity().read(cx).public_key(); cx.background_spawn(async move { + let signer = client.signer().context("Signer not found")?; + let public_key = signer.get_public_key().await?; + let contacts = client.database().contacts_public_keys(public_key).await?; let results = contacts.into_iter().collect(); @@ -700,19 +634,15 @@ impl NostrRegistry { /// Set the metadata for the current user pub fn set_metadata(&self, metadata: &Metadata, cx: &App) -> Task> { let client = self.client(); - let public_key = self.identity().read(cx).public_key(); - let write_relays = self.write_relays(&public_key, cx); let metadata = metadata.clone(); cx.background_spawn(async move { - let urls = write_relays.await; - // Build and sign the metadata event let builder = EventBuilder::metadata(&metadata); let event = client.sign_event_builder(builder).await?; // Send event to user's write relayss - client.send_event(&event).to(urls).await?; + client.send_event(&event).to_nip65().await?; Ok(()) }) @@ -1007,6 +937,29 @@ impl NostrRegistry { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum RelayState { + #[default] + Idle, + Checking, + NotConfigured, + Configured, +} + +impl RelayState { + pub fn idle(&self) -> bool { + matches!(self, RelayState::Idle) + } + + pub fn not_configured(&self) -> bool { + matches!(self, RelayState::NotConfigured) + } + + pub fn configured(&self) -> bool { + matches!(self, RelayState::Configured) + } +} + #[derive(Debug, Clone)] pub struct CoopAuthUrlHandler; diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs index 618df95..afe26bf 100644 --- a/crates/state/src/signer.rs +++ b/crates/state/src/signer.rs @@ -1,4 +1,5 @@ use std::borrow::Cow; +use std::result::Result; use std::sync::Arc; use nostr_sdk::prelude::*; @@ -19,10 +20,12 @@ impl CoopSigner { } } - async fn get(&self) -> Arc { + /// Get the current signer. + pub async fn get(&self) -> Arc { self.signer.read().await.clone() } + /// Switch the current signer to a new signer. pub async fn switch(&self, new: T) where T: IntoNostrSigner, @@ -33,40 +36,40 @@ impl CoopSigner { } impl NostrSigner for CoopSigner { + #[allow(mismatched_lifetime_syntaxes)] fn backend(&self) -> SignerBackend { SignerBackend::Custom(Cow::Borrowed("custom")) } - fn get_public_key(&self) -> BoxedFuture> { - Box::pin(async move { Ok(self.get().await.get_public_key().await?) }) + fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.get_public_key().await }) } - fn sign_event( - &self, + fn sign_event<'a>( + &'a self, unsigned: UnsignedEvent, - ) -> BoxedFuture> { - Box::pin(async move { Ok(self.get().await.sign_event(unsigned).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.sign_event(unsigned).await }) } fn nip04_encrypt<'a>( &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { Ok(self.get().await.nip04_encrypt(public_key, content).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await }) } fn nip04_decrypt<'a>( &'a self, public_key: &'a PublicKey, encrypted_content: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { + ) -> BoxedFuture<'a, Result> { Box::pin(async move { - Ok(self - .get() + self.get() .await .nip04_decrypt(public_key, encrypted_content) - .await?) + .await }) } @@ -74,15 +77,15 @@ impl NostrSigner for CoopSigner { &'a self, public_key: &'a PublicKey, content: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { Ok(self.get().await.nip44_encrypt(public_key, content).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await }) } fn nip44_decrypt<'a>( &'a self, public_key: &'a PublicKey, payload: &'a str, - ) -> BoxedFuture<'a, std::result::Result> { - Box::pin(async move { Ok(self.get().await.nip44_decrypt(public_key, payload).await?) }) + ) -> BoxedFuture<'a, Result> { + Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await }) } }