chore: improve nip4e implementation (#204)

* patch

* update ui

* add load response

* fix

* .

* wip: rewrite gossip

* new gossip implementation

* clean up

* .

* debug

* .

* .

* update

* .

* fix

* fix
This commit is contained in:
reya
2025-11-15 08:30:45 +07:00
committed by GitHub
parent d87bcfbd65
commit 122299f548
18 changed files with 847 additions and 579 deletions

61
Cargo.lock generated
View File

@@ -1205,7 +1205,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1628,7 +1628,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1989,9 +1989,9 @@ dependencies = [
[[package]] [[package]]
name = "exr" name = "exr"
version = "1.73.0" version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
dependencies = [ dependencies = [
"bit_field", "bit_field",
"half", "half",
@@ -2568,7 +2568,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.2.2" version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2665,7 +2665,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2676,7 +2676,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -2905,7 +2905,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -2920,6 +2920,7 @@ dependencies = [
"parking_lot", "parking_lot",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"sha2", "sha2",
"tempfile", "tempfile",
"url", "url",
@@ -2930,7 +2931,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2944,9 +2945,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.7.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" checksum = "1744436df46f0bde35af3eda22aeaba453aada65d8f1c171cd8a5f59030bd69f"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -3735,7 +3736,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3985,7 +3986,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.44.1" version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -4009,7 +4010,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4021,7 +4022,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -4032,7 +4033,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip" name = "nostr-gossip"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"nostr", "nostr",
] ]
@@ -4040,7 +4041,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip-memory" name = "nostr-gossip-memory"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"lru", "lru",
@@ -4052,7 +4053,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flume", "flume",
@@ -4066,7 +4067,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -4083,7 +4084,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.44.1" version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#5508a1a28fa984e577d98238c237dfd2e4dc148c" source = "git+https://github.com/rust-nostr/nostr#641867215b235694353c52af2c1debe920440050"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4594,7 +4595,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
@@ -5220,7 +5221,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@@ -5318,7 +5319,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5372,7 +5373,7 @@ dependencies = [
[[package]] [[package]]
name = "rope" name = "rope"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -5838,7 +5839,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -6286,7 +6287,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -7273,7 +7274,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7309,7 +7310,7 @@ dependencies = [
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#378b30eba5da7b9131b4a1d5bcee5bf09ad567ef" source = "git+https://github.com/zed-industries/zed#cf6ae01d07b5fd02629535250ebddc65f9d0d9ed"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
@@ -7721,9 +7722,9 @@ dependencies = [
[[package]] [[package]]
name = "weezl" name = "weezl"
version = "0.1.11" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8" checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]] [[package]]
name = "which" name = "which"

View File

@@ -1,4 +1,3 @@
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use anyhow::Error; use anyhow::Error;
@@ -21,10 +20,10 @@ pub struct Account {
public_key: Option<PublicKey>, public_key: Option<PublicKey>,
/// Status of the current user NIP-65 relays /// Status of the current user NIP-65 relays
pub nip65_status: RelayStatus, pub nip65_status: Entity<RelayStatus>,
/// Status of the current user NIP-17 relays /// Status of the current user NIP-17 relays
pub nip17_status: RelayStatus, pub nip17_status: Entity<RelayStatus>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>, _tasks: SmallVec<[Task<()>; 2]>,
@@ -64,14 +63,14 @@ impl Account {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let nip65_status = cx.new(|_| RelayStatus::default());
let nip17_status = cx.new(|_| RelayStatus::default());
let mut tasks = smallvec![]; let mut tasks = smallvec![];
tasks.push( tasks.push(
// Observe the nostr signer and set the public key when it sets // Observe the nostr signer and set the public key when it sets
cx.spawn({ cx.spawn(async move |this, cx| {
let client = Arc::clone(&client);
async move |this, cx| {
let result = cx let result = cx
.background_spawn(async move { Self::observe_signer(&client).await }) .background_spawn(async move { Self::observe_signer(&client).await })
.await; .await;
@@ -82,14 +81,13 @@ impl Account {
}) })
.expect("Entity has been released") .expect("Entity has been released")
} }
}
}), }),
); );
Self { Self {
public_key: None, public_key: None,
nip65_status: RelayStatus::default(), nip65_status,
nip17_status: RelayStatus::default(), nip17_status,
_tasks: tasks, _tasks: tasks,
} }
} }
@@ -125,8 +123,6 @@ impl Account {
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?; .await?;
log::info!("Getting user's gossip relays...");
Ok(()) Ok(())
} }
@@ -168,23 +164,29 @@ impl Account {
self._tasks.push( self._tasks.push(
// Verify user's nip65 and nip17 relays // Verify user's nip65 and nip17 relays
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
cx.background_executor() cx.background_executor().timer(Duration::from_secs(5)).await;
.timer(Duration::from_secs(10))
.await;
// Fetch the NIP-65 relays event in the local database
let ensure_nip65 = Self::ensure_nip65_relays(&client, public_key).await; let ensure_nip65 = Self::ensure_nip65_relays(&client, public_key).await;
// Fetch the NIP-17 relays event in the local database
let ensure_nip17 = Self::ensure_nip17_relays(&client, public_key).await; let ensure_nip17 = Self::ensure_nip17_relays(&client, public_key).await;
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.nip65_status = match ensure_nip65 { this.nip65_status.update(cx, |this, cx| {
Ok(true) => RelayStatus::Set, *this = match ensure_nip65 {
_ => RelayStatus::NotSet,
};
this.nip17_status = match ensure_nip17 {
Ok(true) => RelayStatus::Set, Ok(true) => RelayStatus::Set,
_ => RelayStatus::NotSet, _ => RelayStatus::NotSet,
}; };
cx.notify(); cx.notify();
});
this.nip17_status.update(cx, |this, cx| {
*this = match ensure_nip17 {
Ok(true) => RelayStatus::Set,
_ => RelayStatus::NotSet,
};
cx.notify();
});
}) })
.expect("Entity has been released") .expect("Entity has been released")
}), }),

View File

@@ -297,7 +297,7 @@ impl AutoUpdater {
Err(anyhow!("No update available")) Err(anyhow!("No update available"))
} }
} else { } else {
Err(anyhow!("Not found")) Err(anyhow!("No update available"))
} }
}) })
} }

View File

@@ -18,8 +18,7 @@ use nostr_sdk::prelude::*;
pub use room::*; pub use room::*;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::lock::RwLock; use state::{initialized_at, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use state::{initialized_at, EventTracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
mod message; mod message;
mod room; mod room;
@@ -41,6 +40,9 @@ pub struct ChatRegistry {
/// Loading status of the registry /// Loading status of the registry
pub loading: bool, pub loading: bool,
/// Async task for handling notifications
handle_notifications: Task<()>,
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 1]>,
@@ -82,11 +84,19 @@ impl ChatRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let status = Arc::new(AtomicBool::new(true)); let status = Arc::new(AtomicBool::new(true));
let (tx, rx) = flume::bounded::<Signal>(2048); let (tx, rx) = flume::bounded::<Signal>(2048);
let handle_notifications = cx.background_spawn({
let client = nostr.read(cx).client();
let status = Arc::clone(&status);
let tx = tx.clone();
let signer: Option<Arc<dyn NostrSigner>> = None;
async move { Self::handle_notifications(&client, &signer, &tx, &status).await }
});
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![]; let mut tasks = smallvec![];
@@ -98,29 +108,27 @@ impl ChatRegistry {
move |this, state, cx| { move |this, state, cx| {
if let Some(signer) = state.read(cx).clone() { if let Some(signer) = state.read(cx).clone() {
this.retry_failed_events(&signer, &tx, &status, cx); this.handle_notifications = cx.background_spawn({
} let client = nostr.read(cx).client();
}
}),
);
tasks.push(
// Handle notifications
cx.background_spawn({
let client = Arc::clone(&client);
let status = Arc::clone(&status); let status = Arc::clone(&status);
let tx = tx.clone(); let tx = tx.clone();
let signer = Some(signer);
async move { Self::handle_notifications(&client, &tracker, &tx, &status).await } async move {
Self::handle_notifications(&client, &signer, &tx, &status).await
}
});
cx.notify();
}
}
}), }),
); );
tasks.push( tasks.push(
// Handle unwrapping status // Handle unwrapping status
cx.background_spawn({ cx.background_spawn(
let client = Arc::clone(&client); async move { Self::handle_unwrapping(&client, &status, &tx).await },
async move { Self::handle_unwrapping(&client, &status, &tx).await } ),
}),
); );
tasks.push( tasks.push(
@@ -155,23 +163,24 @@ impl ChatRegistry {
Self { Self {
rooms: vec![], rooms: vec![],
loading: true, loading: true,
handle_notifications,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_tasks: tasks, _tasks: tasks,
} }
} }
async fn handle_notifications( async fn handle_notifications<T>(
client: &Client, client: &Client,
tracker: &Arc<RwLock<EventTracker>>, signer: &Option<T>,
tx: &Sender<Signal>, tx: &Sender<Signal>,
status: &Arc<AtomicBool>, status: &Arc<AtomicBool>,
) { ) where
let mut notifications = client.notifications(); T: NostrSigner,
log::info!("Listening for notifications"); {
let initialized_at = initialized_at(); let initialized_at = initialized_at();
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let mut notifications = client.notifications();
let mut public_keys = HashSet::new(); let mut public_keys = HashSet::new();
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
@@ -194,7 +203,7 @@ impl ChatRegistry {
} }
// Extract the rumor from the gift wrap event // Extract the rumor from the gift wrap event
match Self::extract_rumor(client, event.as_ref()).await { match Self::extract_rumor(client, signer, event.as_ref()).await {
Ok(rumor) => { Ok(rumor) => {
// Get all public keys // Get all public keys
public_keys.extend(rumor.all_pubkeys()); public_keys.extend(rumor.all_pubkeys());
@@ -209,7 +218,7 @@ impl ChatRegistry {
Self::get_metadata(client, public_keys).await.ok(); Self::get_metadata(client, public_keys).await.ok();
} }
match &event.created_at >= initialized_at { match &rumor.created_at >= initialized_at {
true => { true => {
let new_message = NewMessage::new(event.id, rumor); let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message); let signal = Signal::Message(new_message);
@@ -223,11 +232,8 @@ impl ChatRegistry {
} }
} }
} }
Err(_e) => { Err(e) => {
let mut tracker = tracker.write().await; log::warn!("Failed to unwrap gift wrap event: {}", e);
tracker.failed_unwrap_events.push(event.as_ref().clone());
drop(tracker);
} }
} }
} }
@@ -280,46 +286,6 @@ impl ChatRegistry {
} }
} }
fn retry_failed_events(
&mut self,
signer: &Arc<dyn NostrSigner>,
tx: &Sender<Signal>,
status: &Arc<AtomicBool>,
cx: &mut Context<Self>,
) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let signer = Arc::clone(signer);
let status = Arc::clone(status);
let tx = tx.clone();
let initialized_at = initialized_at();
self._tasks.push(cx.background_spawn(async move {
let tracker = tracker.read().await;
for event in tracker.failed_unwrap_events.iter() {
if let Ok(rumor) = Self::try_unwrap_custom(&client, &signer, event).await {
match &event.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
if let Err(e) = tx.send_async(signal).await {
log::error!("Failed to send signal: {}", e);
}
}
false => {
status.store(true, Ordering::Release);
}
}
}
}
}));
}
/// Set the loading status of the chat registry /// Set the loading status of the chat registry
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) { pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading; self.loading = loading;
@@ -600,14 +566,21 @@ impl ChatRegistry {
} }
// Unwraps a gift-wrapped event and processes its contents. // Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor(client: &Client, gift_wrap: &Event) -> Result<UnsignedEvent, Error> { async fn extract_rumor<T>(
client: &Client,
signer: &Option<T>,
gift_wrap: &Event,
) -> Result<UnsignedEvent, Error>
where
T: NostrSigner,
{
// Try to get cached rumor first // Try to get cached rumor first
if let Ok(event) = Self::get_rumor(client, gift_wrap.id).await { if let Ok(event) = Self::get_rumor(client, gift_wrap.id).await {
return Ok(event); return Ok(event);
} }
// Try to unwrap with the available signer // Try to unwrap with the available signer
let unwrapped = Self::try_unwrap(client, gift_wrap).await?; let unwrapped = Self::try_unwrap(client, signer, gift_wrap).await?;
let mut rumor_unsigned = unwrapped.rumor; let mut rumor_unsigned = unwrapped.rumor;
// Generate event id for the rumor if it doesn't have one // Generate event id for the rumor if it doesn't have one
@@ -620,34 +593,45 @@ impl ChatRegistry {
} }
// Helper method to try unwrapping with different signers // Helper method to try unwrapping with different signers
async fn try_unwrap(client: &Client, gift_wrap: &Event) -> Result<UnwrappedGift, Error> { async fn try_unwrap<T>(
client: &Client,
signer: &Option<T>,
gift_wrap: &Event,
) -> Result<UnwrappedGift, Error>
where
T: NostrSigner,
{
if let Some(custom_signer) = signer.as_ref() {
if let Ok(seal) = custom_signer
.nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content)
.await
{
let seal: Event = Event::from_json(seal)?;
seal.verify_with_ctx(SECP256K1)?;
// Decrypt the rumor
// TODO: verify the sender
let rumor = custom_signer
.nip44_decrypt(&seal.pubkey, &seal.content)
.await?;
// Construct the unsigned event
let rumor = UnsignedEvent::from_json(rumor)?;
// Return the unwrapped gift
return Ok(UnwrappedGift {
sender: rumor.pubkey,
rumor,
});
}
}
let signer = client.signer().await?; let signer = client.signer().await?;
let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?; let unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;
Ok(unwrapped) Ok(unwrapped)
} }
/// Helper method to try unwrapping with a custom signer
async fn try_unwrap_custom<T>(
client: &Client,
signer: &T,
gift_wrap: &Event,
) -> Result<UnsignedEvent, Error>
where
T: NostrSigner,
{
let unwrapped = UnwrappedGift::from_gift_wrap(signer, gift_wrap).await?;
let mut rumor_unsigned = unwrapped.rumor;
// Generate event id for the rumor if it doesn't have one
rumor_unsigned.ensure_id();
// Cache the rumor
Self::set_rumor(client, gift_wrap.id, &rumor_unsigned).await?;
Ok(rumor_unsigned)
}
/// Stores an unwrapped event in local database with reference to original /// Stores an unwrapped event in local database with reference to original
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> { async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
let rumor_id = rumor.id.context("Rumor is missing an event id")?; let rumor_id = rumor.id.context("Rumor is missing an event id")?;
@@ -718,7 +702,7 @@ impl ChatRegistry {
{ {
let authors: Vec<PublicKey> = public_keys.into_iter().collect(); let authors: Vec<PublicKey> = public_keys.into_iter().collect();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList];
// Return if the list is empty // Return if the list is empty
if authors.is_empty() { if authors.is_empty() {
@@ -726,7 +710,7 @@ impl ChatRegistry {
} }
let filter = Filter::new() let filter = Filter::new()
.limit(authors.len() * kinds.len() + 10) .limit(authors.len() * kinds.len())
.authors(authors) .authors(authors)
.kinds(kinds); .kinds(kinds);

View File

@@ -8,6 +8,7 @@ use anyhow::{anyhow, Error};
use common::{EventUtils, RenderedProfile}; use common::{EventUtils, RenderedProfile};
use encryption::{Encryption, SignerKind}; use encryption::{Encryption, SignerKind};
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task};
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use state::NostrRegistry; use state::NostrRegistry;
@@ -326,7 +327,7 @@ impl Room {
cx.emit(RoomSignal::Refresh); cx.emit(RoomSignal::Refresh);
} }
/// Get messaging relays and encryption keys announcement for each member /// Get gossip relays for each member
pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> { pub fn connect(&self, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
@@ -342,22 +343,10 @@ impl Room {
continue; continue;
}; };
// Construct a filter for messaging relays // Construct a filter for gossip relays
let filter = Filter::new() let filter = Filter::new().kind(Kind::RelayList).author(member).limit(1);
.kind(Kind::InboxRelays)
.author(member)
.limit(1);
// Subscribe to get members messaging relays // Subscribe to get member's gossip relays
client.subscribe(filter, Some(opts)).await?;
// Construct a filter for encryption keys announcement
let filter = Filter::new()
.kind(Kind::Custom(10044))
.author(member)
.limit(1);
// Subscribe to get members encryption keys announcement
client.subscribe(filter, Some(opts)).await?; client.subscribe(filter, Some(opts)).await?;
} }
@@ -376,15 +365,15 @@ impl Room {
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id); .custom_tag(SingleLetterTag::lowercase(Alphabet::C), conversation_id);
let stored = client.database().query(filter).await?; let messages = client
.database()
let mut messages: Vec<UnsignedEvent> = stored .query(filter)
.await?
.into_iter() .into_iter()
.filter_map(|event| UnsignedEvent::from_json(&event.content).ok()) .filter_map(|event| UnsignedEvent::from_json(&event.content).ok())
.sorted_by_key(|message| message.created_at)
.collect(); .collect();
messages.sort_by_key(|message| message.created_at);
Ok(messages) Ok(messages)
}) })
} }
@@ -392,8 +381,8 @@ impl Room {
/// Create a new message event (unsigned) /// Create a new message event (unsigned)
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let cache = nostr.read(cx).cache_manager(); let gossip = nostr.read(cx).gossip();
let cache = cache.read_blocking(); let read_gossip = gossip.read_blocking();
// Get current user // Get current user
let account = Account::global(cx); let account = Account::global(cx);
@@ -409,9 +398,7 @@ impl Room {
// NOTE: current user will be removed from the list of receivers // NOTE: current user will be removed from the list of receivers
for member in self.members.iter() { for member in self.members.iter() {
// Get relay hint if available // Get relay hint if available
let relay_url = cache let relay_url = read_gossip.messaging_relays(member).first().cloned();
.relay(member)
.and_then(|urls| urls.iter().nth(0).cloned());
// Construct a public key tag with relay hint // Construct a public key tag with relay hint
let tag = TagStandard::PublicKey { let tag = TagStandard::PublicKey {
@@ -470,7 +457,7 @@ impl Room {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let cache = nostr.read(cx).cache_manager(); let gossip = nostr.read(cx).gossip();
let tracker = nostr.read(cx).tracker(); let tracker = nostr.read(cx).tracker();
let rumor = rumor.to_owned(); let rumor = rumor.to_owned();
@@ -481,8 +468,11 @@ impl Room {
cx.background_spawn(async move { cx.background_spawn(async move {
let signer_kind = opts.signer_kind; let signer_kind = opts.signer_kind;
let cache = cache.read().await; let gossip = gossip.read().await;
let tracker = tracker.read().await;
// Get current user's signer and public key
let user_signer = client.signer().await?;
let user_pubkey = user_signer.get_public_key().await?;
// Get the encryption public key // Get the encryption public key
let encryption_pubkey = if let Some(signer) = encryption_key.as_ref() { let encryption_pubkey = if let Some(signer) = encryption_key.as_ref() {
@@ -491,9 +481,6 @@ impl Room {
None None
}; };
let user_signer = client.signer().await?;
let user_pubkey = user_signer.get_public_key().await?;
// Remove the current user's public key from the list of receivers // Remove the current user's public key from the list of receivers
// the current user will be handled separately // the current user will be handled separately
members.retain(|&pk| pk != user_pubkey); members.retain(|&pk| pk != user_pubkey);
@@ -506,7 +493,9 @@ impl Room {
for member in members.into_iter() { for member in members.into_iter() {
// Get user's messaging relays // Get user's messaging relays
let urls = cache.relay(&member).cloned().unwrap_or_default(); let urls = gossip.messaging_relays(&member);
// Get user's encryption public key if available
let encryption = gossip.announcement(&member).map(|a| a.public_key());
// Check if there are any relays to send the message to // Check if there are any relays to send the message to
if urls.is_empty() { if urls.is_empty() {
@@ -514,35 +503,26 @@ impl Room {
continue; continue;
} }
// Ensure connection to all messaging relays
for url in urls.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Get user's encryption public key if available
let encryption = cache
.announcement(&member)
.and_then(|a| a.to_owned().map(|a| a.public_key()));
// Skip sending if using encryption signer but receiver's encryption keys not found // Skip sending if using encryption signer but receiver's encryption keys not found
if encryption.is_none() && matches!(signer_kind, SignerKind::Encryption) { if encryption.is_none() && matches!(signer_kind, SignerKind::Encryption) {
reports.push(SendReport::new(member).device_not_found()); reports.push(SendReport::new(member).device_not_found());
continue; continue;
} }
let receiver = Self::select_receiver(&signer_kind, member, encryption)?; // Ensure connections to the relays
let rumor = rumor.clone(); gossip.ensure_connections(&client, &urls).await;
// Construct the sealed event // Determine the receiver based on the signer kind
let seal = EventBuilder::seal(&signer, &receiver, rumor.clone()) let receiver = Self::select_receiver(&signer_kind, member, encryption)?;
.await?
.build(member)
.sign(&signer)
.await?;
// Construct the gift wrap event // Construct the gift wrap event
let event = EventBuilder::gift_wrap_from_seal(&member, &seal, vec![])?; let event = EventBuilder::gift_wrap(
&signer,
&receiver,
rumor.clone(),
vec![Tag::public_key(member)],
)
.await?;
// Send the gift wrap event to the messaging relays // Send the gift wrap event to the messaging relays
match client.send_event_to(urls, &event).await { match client.send_event_to(urls, &event).await {
@@ -554,6 +534,7 @@ impl Room {
if auth { if auth {
// Wait for authenticated and resent event successfully // Wait for authenticated and resent event successfully
for attempt in 0..=SEND_RETRY { for attempt in 0..=SEND_RETRY {
let tracker = tracker.read().await;
let ids = tracker.resent_ids(); let ids = tracker.resent_ids();
// Check if event was successfully resent // Check if event was successfully resent
@@ -581,22 +562,34 @@ impl Room {
} }
} }
let receiver = Self::select_receiver(&signer_kind, user_pubkey, encryption_pubkey)?; // Return early if the user disabled backup.
let rumor = rumor.clone(); //
// Coop will not send a gift wrap event to the current user.
if !opts.backup() {
return Ok(reports);
}
// Construct the sealed event // Skip sending if using encryption signer but receiver's encryption keys not found
let seal = EventBuilder::seal(&signer, &receiver, rumor.clone()) if encryption_pubkey.is_none() && matches!(signer_kind, SignerKind::Encryption) {
.await? reports.push(SendReport::new(user_pubkey).device_not_found());
.build(user_pubkey) return Ok(reports);
.sign(&signer) }
.await?;
// Determine the receiver based on the signer kind
let receiver = Self::select_receiver(&signer_kind, user_pubkey, encryption_pubkey)?;
// Construct the gift-wrapped event // Construct the gift-wrapped event
let event = EventBuilder::gift_wrap_from_seal(&receiver, &seal, vec![])?; let event = EventBuilder::gift_wrap(
&signer,
&receiver,
rumor.clone(),
vec![Tag::public_key(user_pubkey)],
)
.await?;
// Only send a backup message to current user if sent successfully to others // Only send a backup message to current user if sent successfully to others
if opts.backup() && reports.iter().all(|r| r.is_sent_success()) { if reports.iter().all(|r| r.is_sent_success()) {
let urls = cache.relay(&user_pubkey).cloned().unwrap_or_default(); let urls = gossip.messaging_relays(&user_pubkey);
// Check if there are any relays to send the event to // Check if there are any relays to send the event to
if urls.is_empty() { if urls.is_empty() {
@@ -604,11 +597,8 @@ impl Room {
return Ok(reports); return Ok(reports);
} }
// Ensure connection to all messaging relays // Ensure connections to the relays
for url in urls.iter() { gossip.ensure_connections(&client, &urls).await;
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Send the event to the messaging relays // Send the event to the messaging relays
match client.send_event_to(urls, &event).await { match client.send_event_to(urls, &event).await {
@@ -635,10 +625,10 @@ impl Room {
) -> Task<Result<Vec<SendReport>, Error>> { ) -> Task<Result<Vec<SendReport>, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let cache_manager = nostr.read(cx).cache_manager(); let gossip = nostr.read(cx).gossip();
cx.background_spawn(async move { cx.background_spawn(async move {
let cache = cache_manager.read().await; let gossip = gossip.read().await;
let mut resend_reports = vec![]; let mut resend_reports = vec![];
for report in reports.into_iter() { for report in reports.into_iter() {
@@ -667,12 +657,15 @@ impl Room {
// Process the on hold event if it exists // Process the on hold event if it exists
if let Some(event) = report.on_hold { if let Some(event) = report.on_hold {
let urls = cache.relay(&receiver).cloned().unwrap_or_default(); let urls = gossip.messaging_relays(&receiver);
// Check if there are any relays to send the event to // Check if there are any relays to send the event to
if urls.is_empty() { if urls.is_empty() {
resend_reports.push(SendReport::new(receiver).relays_not_found()); resend_reports.push(SendReport::new(receiver).relays_not_found());
} else { } else {
// Ensure connections to the relays
gossip.ensure_connections(&client, &urls).await;
// Send the event to the messaging relays // Send the event to the messaging relays
match client.send_event_to(urls, &event).await { match client.send_event_to(urls, &event).await {
Ok(output) => { Ok(output) => {
@@ -705,15 +698,15 @@ impl Room {
fn select_receiver( fn select_receiver(
kind: &SignerKind, kind: &SignerKind,
members: PublicKey, member: PublicKey,
encryption: Option<PublicKey>, encryption: Option<PublicKey>,
) -> Result<PublicKey, Error> { ) -> Result<PublicKey, Error> {
match kind { match kind {
SignerKind::Encryption => { SignerKind::Encryption => {
Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?) Ok(encryption.ok_or_else(|| anyhow!("Receiver's encryption key not found"))?)
} }
SignerKind::User => Ok(members), SignerKind::User => Ok(member),
SignerKind::Auto => Ok(encryption.unwrap_or(members)), SignerKind::Auto => Ok(encryption.unwrap_or(member)),
} }
} }
} }

View File

@@ -272,13 +272,13 @@ impl ChatPanel {
// Get the current room entity // Get the current room entity
let room = self.room.read(cx); let room = self.room.read(cx);
let opts = self.options.read(cx);
// Create a temporary message for optimistic update // Create a temporary message for optimistic update
let rumor = room.create_message(&content, replies.as_ref(), cx); let rumor = room.create_message(&content, replies.as_ref(), cx);
let rumor_id = rumor.id.unwrap(); let rumor_id = rumor.id.unwrap();
// Create a task for sending the message in the background // Create a task for sending the message in the background
let opts = self.options.read(cx);
let send_message = room.send_message(&rumor, opts, cx); let send_message = room.send_message(&rumor, opts, cx);
// Optimistically update message list // Optimistically update message list

View File

@@ -197,7 +197,7 @@ impl Compose {
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> { async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10); let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
client client

View File

@@ -175,9 +175,6 @@ impl EditProfile {
} }
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> { pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let avatar = self.avatar_input.read(cx).value().to_string(); let avatar = self.avatar_input.read(cx).value().to_string();
let name = self.name_input.read(cx).value().to_string(); let name = self.name_input.read(cx).value().to_string();
let bio = self.bio_input.read(cx).value().to_string(); let bio = self.bio_input.read(cx).value().to_string();
@@ -199,14 +196,24 @@ impl EditProfile {
new_metadata = new_metadata.website(url); new_metadata = new_metadata.website(url);
} }
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
cx.background_spawn(async move { cx.background_spawn(async move {
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&public_key);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
// Sign the new metadata event // Sign the new metadata event
let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?; let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?;
// Send event to user's write relayss // Send event to user's write relayss
client.send_event(&event).await?; client.send_event_to(write_relays, &event).await?;
// Return the updated profile // Return the updated profile
let metadata = Metadata::from_json(&event.content).unwrap_or_default(); let metadata = Metadata::from_json(&event.content).unwrap_or_default();

View File

@@ -137,11 +137,12 @@ impl NewAccount {
// Verify the signer // Verify the signer
let signer = client.signer().await?; let signer = client.signer().await?;
let nip65_relays = default_nip65_relays();
// Construct a NIP-65 event // Construct a NIP-65 event
let event = EventBuilder::new(Kind::RelayList, "") let event = EventBuilder::new(Kind::RelayList, "")
.tags( .tags(
default_nip65_relays() nip65_relays
.iter() .iter()
.cloned() .cloned()
.map(|(url, metadata)| Tag::relay_metadata(url, metadata)), .map(|(url, metadata)| Tag::relay_metadata(url, metadata)),
@@ -152,6 +153,25 @@ impl NewAccount {
// Set NIP-65 relays // Set NIP-65 relays
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?; client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
// Ensure relays are connected
for (url, _metadata) in nip65_relays.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Extract only write relays
let write_relays: Vec<RelayUrl> = nip65_relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url.to_owned())
} else {
None
}
})
.take(3)
.collect();
// Construct a NIP-17 event // Construct a NIP-17 event
let event = EventBuilder::new(Kind::InboxRelays, "") let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(default_nip17_relays().iter().cloned().map(Tag::relay)) .tags(default_nip17_relays().iter().cloned().map(Tag::relay))
@@ -159,13 +179,13 @@ impl NewAccount {
.await?; .await?;
// Set NIP-17 relays // Set NIP-17 relays
client.send_event(&event).await?; client.send_event_to(&write_relays, &event).await?;
// Construct a metadata event // Construct a metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?; let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
// Set metadata // Set metadata
client.send_event(&event).await?; client.send_event_to(&write_relays, &event).await?;
Ok(()) Ok(())
}); });

View File

@@ -1,4 +1,3 @@
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS}; use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
@@ -44,7 +43,7 @@ impl Screening {
let mut tasks = smallvec![]; let mut tasks = smallvec![];
let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({ let contact_check: Task<Result<(bool, Vec<Profile>), Error>> = cx.background_spawn({
let client = Arc::clone(&client); let client = nostr.read(cx).client();
async move { async move {
let signer = client.signer().await?; let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?; let signer_pubkey = signer.get_public_key().await?;

View File

@@ -155,10 +155,17 @@ impl SetupRelay {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let relays = self.relays.clone(); let relays = self.relays.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&public_key);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
let tags: Vec<Tag> = relays let tags: Vec<Tag> = relays
.iter() .iter()
@@ -171,7 +178,7 @@ impl SetupRelay {
.await?; .await?;
// Set messaging relays // Set messaging relays
client.send_event(&event).await?; client.send_event_to(write_relays, &event).await?;
// Connect to messaging relays // Connect to messaging relays
for relay in relays.iter() { for relay in relays.iter() {

View File

@@ -137,15 +137,13 @@ impl Sidebar {
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> { async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList];
let filter = Filter::new().author(public_key).kinds(kinds).limit(10); let filter = Filter::new().author(public_key).kinds(kinds).limit(10);
client client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?; .await?;
log::info!("Subscribe to get metadata for: {public_key}");
Ok(()) Ok(())
} }

View File

@@ -49,10 +49,10 @@ pub struct Encryption {
handle_requests: Option<Task<()>>, handle_requests: Option<Task<()>>,
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 2]>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>, _tasks: SmallVec<[Task<()>; 1]>,
} }
impl Encryption { impl Encryption {
@@ -69,48 +69,36 @@ impl Encryption {
/// Create a new encryption instance /// Create a new encryption instance
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
let account = Account::global(cx); let account = Account::global(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let requests = cx.new(|_| HashSet::default()); let requests = cx.new(|_| HashSet::default());
let encryption = cx.new(|_| None); let encryption = cx.new(|_| None);
let client_signer = cx.new(|_| None); let client_signer = cx.new(|_| None);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push( subscriptions.push(
// Observe the account state // Observe the account state
cx.observe(&account, |this, state, cx| { cx.observe(&account, |this, state, cx| {
if state.read(cx).has_account() && !this.has_encryption(cx) { if state.read(cx).has_account() && this.client_signer.read(cx).is_none() {
this.get_client(cx);
}
}),
);
subscriptions.push(
// Observe the client signer state
cx.observe(&client_signer, |this, state, cx| {
if state.read(cx).is_some() {
this.get_announcement(cx); this.get_announcement(cx);
} }
}), }),
); );
tasks.push( subscriptions.push(
// Get the client key // Observe the encryption signer state
cx.spawn(async move |this, cx| { cx.observe(&encryption, |this, state, cx| {
match Self::get_keys(&client, "client").await { if state.read(cx).is_some() {
Ok(keys) => { this._tasks.push(this.resubscribe_messages(cx));
this.update(cx, |this, cx| {
this.set_client(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
Err(_) => {
let keys = Keys::generate();
let secret = keys.secret_key().to_secret_hex();
// Store the key in the database for future use
Self::set_keys(&client, "client", secret).await.ok();
// Update global state
this.update(cx, |this, cx| {
this.set_client(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
} }
}), }),
); );
@@ -123,7 +111,7 @@ impl Encryption {
handle_notifications: None, handle_notifications: None,
handle_requests: None, handle_requests: None,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_tasks: tasks, _tasks: smallvec![],
} }
} }
@@ -174,14 +162,50 @@ impl Encryption {
} }
} }
/// Get the client keys from the database
fn get_client(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
self._tasks.push(
// Run in the main thread
cx.spawn(async move |this, cx| {
match Self::get_keys(&client, "client").await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_client(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
Err(_) => {
let keys = Keys::generate();
let secret = keys.secret_key().to_secret_hex();
// Store the key in the database for future use
Self::set_keys(&client, "client", secret).await.ok();
// Update global state
this.update(cx, |this, cx| {
this.set_client(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
}
}),
)
}
/// Get the announcement from the database
fn get_announcement(&mut self, cx: &mut Context<Self>) { fn get_announcement(&mut self, cx: &mut Context<Self>) {
let task = self._get_announcement(cx); let task = self._get_announcement(cx);
let delay = Duration::from_secs(10);
self._tasks.push(cx.spawn(async move |this, cx| { self._tasks.push(
cx.background_executor().timer(Duration::from_secs(5)).await; // Run task in the background
cx.spawn(async move |this, cx| {
cx.background_executor().timer(delay).await;
match task.await { if let Ok(announcement) = task.await {
Ok(announcement) => {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.load_encryption(&announcement, cx); this.load_encryption(&announcement, cx);
// Set the announcement // Set the announcement
@@ -190,11 +214,8 @@ impl Encryption {
}) })
.expect("Entity has been released"); .expect("Entity has been released");
} }
Err(err) => { }),
log::error!("Failed to get announcement: {}", err); );
}
};
}));
} }
fn _get_announcement(&self, cx: &App) -> Task<Result<Announcement, Error>> { fn _get_announcement(&self, cx: &App) -> Task<Result<Announcement, Error>> {
@@ -236,12 +257,65 @@ impl Encryption {
this.listen_request(cx); this.listen_request(cx);
} }
} }
this.load_response(cx);
}) })
.expect("Entity has been released"); .expect("Entity has been released");
}) })
.detach(); .detach();
} }
pub fn load_response(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else {
return;
};
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.author(public_key)
.kind(Kind::Custom(4455))
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let response = NostrRegistry::extract_response(&client, &event).await?;
// Decrypt the payload using the client signer
let decrypted = client_signer
.nip44_decrypt(&response.public_key(), response.payload())
.await?;
// Construct the encryption keys
let secret = SecretKey::parse(&decrypted)?;
let keys = Keys::new(secret);
return Ok(keys);
}
Err(anyhow!("not found"))
});
cx.spawn(async move |this, cx| {
match task.await {
Ok(keys) => {
this.update(cx, |this, cx| {
this.set_encryption(Arc::new(keys), cx);
})
.expect("Entity has been released");
}
Err(e) => {
log::warn!("Failed to load encryption response: {e}");
}
};
})
.detach();
}
/// Listen for the encryption key request from other devices /// Listen for the encryption key request from other devices
/// ///
/// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
@@ -252,7 +326,7 @@ impl Encryption {
let (tx, rx) = flume::bounded::<Announcement>(50); let (tx, rx) = flume::bounded::<Announcement>(50);
let task: Task<Result<(), Error>> = cx.background_spawn({ let task: Task<Result<(), Error>> = cx.background_spawn({
let client = Arc::clone(&client); let client = nostr.read(cx).client();
async move { async move {
let signer = client.signer().await?; let signer = client.signer().await?;
@@ -325,6 +399,7 @@ impl Encryption {
pub fn new_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> { pub fn new_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let keys = Keys::generate(); let keys = Keys::generate();
let public_key = keys.public_key(); let public_key = keys.public_key();
@@ -336,6 +411,12 @@ impl Encryption {
Self::set_keys(&client, "encryption", secret).await?; Self::set_keys(&client, "encryption", secret).await?;
let signer = client.signer().await?; let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&signer_pubkey);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
// Construct the announcement event // Construct the announcement event
let event = EventBuilder::new(Kind::Custom(10044), "") let event = EventBuilder::new(Kind::Custom(10044), "")
@@ -343,11 +424,12 @@ impl Encryption {
Tag::client(app_name()), Tag::client(app_name()),
Tag::custom(TagKind::custom("n"), vec![public_key]), Tag::custom(TagKind::custom("n"), vec![public_key]),
]) ])
.build(signer_pubkey)
.sign(&signer) .sign(&signer)
.await?; .await?;
// Send the announcement event to user's relays // Send the announcement event to user's relays
client.send_event(&event).await?; client.send_event_to(write_relays, &event).await?;
Ok(keys) Ok(keys)
}) })
@@ -359,6 +441,7 @@ impl Encryption {
pub fn send_request(&self, cx: &App) -> Task<Result<Option<Keys>, Error>> { pub fn send_request(&self, cx: &App) -> Task<Result<Option<Keys>, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
// Get the client signer // Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else { let Some(client_signer) = self.client_signer.read(cx).clone() else {
@@ -395,6 +478,12 @@ impl Encryption {
Ok(Some(keys)) Ok(Some(keys))
} }
None => { None => {
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&public_key);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
// Construct encryption keys request event // Construct encryption keys request event
let event = EventBuilder::new(Kind::Custom(4454), "") let event = EventBuilder::new(Kind::Custom(4454), "")
.tags(vec![ .tags(vec![
@@ -405,7 +494,7 @@ impl Encryption {
.await?; .await?;
// Send a request for encryption keys from other devices // Send a request for encryption keys from other devices
client.send_event(&event).await?; client.send_event_to(&write_relays, &event).await?;
// Create a unique ID to control the subscription later // Create a unique ID to control the subscription later
let subscription_id = SubscriptionId::new("listen-response"); let subscription_id = SubscriptionId::new("listen-response");
@@ -413,12 +502,11 @@ impl Encryption {
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::Custom(4455)) .kind(Kind::Custom(4455))
.author(public_key) .author(public_key)
.pubkey(client_pubkey)
.since(Timestamp::now()); .since(Timestamp::now());
// Subscribe to the approval response event // Subscribe to the approval response event
client client
.subscribe_with_id(subscription_id, filter, None) .subscribe_with_id_to(&write_relays, subscription_id, filter, None)
.await?; .await?;
Ok(None) Ok(None)
@@ -433,6 +521,7 @@ impl Encryption {
pub fn send_response(&self, target: PublicKey, cx: &App) -> Task<Result<(), Error>> { pub fn send_response(&self, target: PublicKey, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
// Get the client signer // Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else { let Some(client_signer) = self.client_signer.read(cx).clone() else {
@@ -440,6 +529,14 @@ impl Encryption {
}; };
cx.background_spawn(async move { cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&public_key);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
let encryption = Self::get_keys(&client, "encryption").await?; let encryption = Self::get_keys(&client, "encryption").await?;
let client_pubkey = client_signer.get_public_key().await?; let client_pubkey = client_signer.get_public_key().await?;
@@ -457,30 +554,12 @@ impl Encryption {
Tag::custom(TagKind::custom("P"), vec![client_pubkey]), Tag::custom(TagKind::custom("P"), vec![client_pubkey]),
Tag::public_key(target), Tag::public_key(target),
]) ])
.sign(&client_signer) .build(public_key)
.sign(&signer)
.await?; .await?;
// Get the current user's signer and public key
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
// Get the current user's relay list
let urls: Vec<RelayUrl> = client
.database()
.relay_list(public_key)
.await?
.into_iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == Some(RelayMetadata::Read) {
Some(url)
} else {
None
}
})
.collect();
// Send the response event to the user's relay list // Send the response event to the user's relay list
client.send_event_to(urls, &event).await?; client.send_event_to(write_relays, &event).await?;
Ok(()) Ok(())
}) })
@@ -493,12 +572,14 @@ impl Encryption {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let client_signer = self.client_signer.read(cx).clone().unwrap(); // Get the client signer
let mut processed_events = HashSet::new(); let Some(client_signer) = self.client_signer.read(cx).clone() else {
return Task::ready(Err(anyhow!("Client Signer is required")));
};
cx.background_spawn(async move { cx.background_spawn(async move {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
log::info!("Listening for notifications"); let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await { while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else { let RelayPoolNotification::Message { message, .. } = notification else {
@@ -513,7 +594,7 @@ impl Encryption {
} }
if event.kind != Kind::Custom(4455) { if event.kind != Kind::Custom(4455) {
// Skip non-gift wrap events // Skip non-response events
continue; continue;
} }
@@ -528,6 +609,8 @@ impl Encryption {
let keys = Keys::new(secret); let keys = Keys::new(secret);
return Ok(keys); return Ok(keys);
} else {
log::error!("Failed to extract response from event");
} }
} }
} }
@@ -579,4 +662,21 @@ impl Encryption {
cx.notify(); cx.notify();
}); });
} }
/// Resubscribe to gift wrap events
fn resubscribe_messages(&self, cx: &App) -> Task<()> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
cx.background_spawn(async move {
let gossip = gossip.read().await;
let relays = gossip.messaging_relays(&public_key);
NostrRegistry::get_messages(&client, public_key, &relays).await;
})
}
} }

View File

@@ -1,3 +1,5 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -181,6 +183,7 @@ impl EncryptionPanel {
}); });
} }
Err(e) => { Err(e) => {
this.set_requesting(false, cx);
this.set_error(e.to_string(), cx); this.set_error(e.to_string(), cx);
} }
}; };
@@ -193,11 +196,13 @@ impl EncryptionPanel {
fn ask_for_approval(&mut self, req: Announcement, window: &mut Window, cx: &mut Context<Self>) { fn ask_for_approval(&mut self, req: Announcement, window: &mut Window, cx: &mut Context<Self>) {
let client_name = SharedString::from(req.client().to_string()); let client_name = SharedString::from(req.client().to_string());
let target = req.public_key(); let target = req.public_key();
let id = SharedString::from(req.id().to_hex());
let loading = Rc::new(Cell::new(false));
let note = Notification::new() let note = Notification::new()
.custom_id(SharedString::from(req.id().to_hex())) .custom_id(id.clone())
.autohide(false) .autohide(false)
.icon(IconName::Info) .icon(IconName::Encryption)
.title(SharedString::from("Encryption Key Request")) .title(SharedString::from("Encryption Key Request"))
.content(move |_window, cx| { .content(move |_window, cx| {
v_flex() v_flex()
@@ -208,14 +213,24 @@ impl EncryptionPanel {
)) ))
.child( .child(
v_flex() v_flex()
.py_1() .h_12()
.px_1p5() .items_center()
.rounded_sm() .justify_center()
.text_xs() .px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().warning_background) .bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground) .text_color(cx.theme().warning_foreground)
.child(client_name.clone()), .child(client_name.clone()),
) )
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(SharedString::from(target.to_hex())),
)
.into_any_element() .into_any_element()
}) })
.action(move |_window, _cx| { .action(move |_window, _cx| {
@@ -223,13 +238,38 @@ impl EncryptionPanel {
.label("Approve") .label("Approve")
.small() .small()
.primary() .primary()
.loading(false) .loading(loading.get())
.disabled(false) .disabled(loading.get())
.on_click(move |_ev, _window, cx| { .on_click({
let loading = Rc::clone(&loading);
let id = id.clone();
move |_ev, window, cx| {
// Set loading state to true
loading.set(true);
let encryption = Encryption::global(cx); let encryption = Encryption::global(cx);
let send_response = encryption.read(cx).send_response(target, cx); let send_response = encryption.read(cx).send_response(target, cx);
let id = id.clone();
send_response.detach(); window
.spawn(cx, async move |cx| {
let result = send_response.await;
cx.update(|window, cx| {
match result {
Ok(_) => {
window.clear_notification_by_id(id, cx);
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.expect("Entity has been released");
})
.detach();
}
}) })
}); });
@@ -252,6 +292,7 @@ impl Render for EncryptionPanel {
const WARNING: &str = "Encryption Key is still in the alpha stage. Please be cautious."; const WARNING: &str = "Encryption Key is still in the alpha stage. Please be cautious.";
let encryption = Encryption::global(cx); let encryption = Encryption::global(cx);
let announcement = encryption.read(cx).announcement();
let has_encryption = encryption.read(cx).has_encryption(cx); let has_encryption = encryption.read(cx).has_encryption(cx);
v_flex() v_flex()
@@ -259,39 +300,49 @@ impl Render for EncryptionPanel {
.max_w(px(340.)) .max_w(px(340.))
.w(px(340.)) .w(px(340.))
.text_sm() .text_sm()
.when(has_encryption, |this| { .when_some(announcement.as_ref(), |this, announcement| {
this.child(
h_flex()
.gap_2()
.w_full()
.text_xs()
.font_semibold()
.child(
Icon::new(IconName::CheckCircleFill)
.small()
.text_color(cx.theme().element_active),
)
.child(SharedString::from("Encryption Key has been set")),
)
})
.when(!has_encryption, |this| {
if let Some(announcement) = encryption.read(cx).announcement().as_ref() {
let pubkey = shorten_pubkey(announcement.public_key(), 16); let pubkey = shorten_pubkey(announcement.public_key(), 16);
let name = announcement.client(); let name = announcement.client();
this.child( this.child(
v_flex() v_flex()
.gap_2() .gap_2()
.child(div().font_semibold().child(SharedString::from(NOTICE))) .when(has_encryption, |this| {
this.child(
h_flex()
.gap_1p5()
.text_sm()
.font_semibold()
.child(
Icon::new(IconName::CheckCircle)
.text_color(cx.theme().element_foreground)
.small(),
)
.child(SharedString::from("Encryption Key has been set")),
)
})
.when(!has_encryption, |this| {
this.child(div().font_semibold().child(SharedString::from(NOTICE)))
})
.child( .child(
v_flex() v_flex()
.gap_1()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Client Name:")),
)
.child(
h_flex()
.h_12() .h_12()
.items_center() .items_center()
.justify_center() .justify_center()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.bg(cx.theme().warning_background) .bg(cx.theme().elevated_surface_background)
.text_color(cx.theme().warning_foreground)
.child(name), .child(name),
),
) )
.child( .child(
v_flex() v_flex()
@@ -313,6 +364,10 @@ impl Render for EncryptionPanel {
.child(SharedString::from(pubkey)), .child(SharedString::from(pubkey)),
), ),
) )
.when(!has_encryption, |this| {
this.child(
v_flex()
.gap_2()
.child( .child(
div() div()
.text_xs() .text_xs()
@@ -361,18 +416,12 @@ impl Render for EncryptionPanel {
)), )),
) )
}), }),
) ),
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_foreground)
.child(error.clone()),
) )
}), }),
) )
} else { })
.when_none(&announcement, |this| {
this.child( this.child(
v_flex() v_flex()
.gap_2() .gap_2()
@@ -401,7 +450,15 @@ impl Render for EncryptionPanel {
})), })),
), ),
) )
} })
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_foreground)
.child(error.clone()),
)
}) })
} }
} }

View File

@@ -1,5 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use gpui::{App, AppContext, Context, Entity, Global, Task}; use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -44,12 +43,10 @@ impl PersonRegistry {
tasks.push( tasks.push(
// Handle notifications // Handle notifications
cx.spawn({ cx.spawn({
let client = Arc::clone(&client); let client = nostr.read(cx).client();
async move |this, cx| { async move |this, cx| {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
log::info!("Listening for notifications");
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await { while let Ok(notification) = notifications.recv().await {

View File

@@ -16,8 +16,7 @@ pub use tracker::*;
mod storage; mod storage;
mod tracker; mod tracker;
pub const GIFTWRAP_SUBSCRIPTION: &str = "default-inbox"; pub const GIFTWRAP_SUBSCRIPTION: &str = "gift-wrap-events";
pub const ENCRYPTION_GIFTWARP_SUBSCRIPTION: &str = "encryption-inbox";
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
@@ -30,15 +29,15 @@ impl Global for GlobalNostrRegistry {}
/// Nostr Registry /// Nostr Registry
#[derive(Debug)] #[derive(Debug)]
pub struct NostrRegistry { pub struct NostrRegistry {
/// Nostr client instance /// Nostr Client
client: Arc<Client>, client: Client,
/// Custom gossip implementation
gossip: Arc<RwLock<Gossip>>,
/// Tracks activity related to Nostr events /// Tracks activity related to Nostr events
tracker: Arc<RwLock<EventTracker>>, tracker: Arc<RwLock<EventTracker>>,
/// Manages caching of nostr events
cache_manager: Arc<RwLock<CacheManager>>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>, _tasks: SmallVec<[Task<()>; 1]>,
} }
@@ -63,11 +62,7 @@ impl NostrRegistry {
.install_default() .install_default()
.ok(); .ok();
let path = config_dir().join("nostr"); // Construct the nostr client options
let lmdb = NostrLMDB::open(path).expect("Failed to initialize database");
let gossip = NostrGossipMemory::unbounded();
// Nostr client options
let opts = ClientOptions::new() let opts = ClientOptions::new()
.automatic_authentication(false) .automatic_authentication(false)
.verify_subscriptions(false) .verify_subscriptions(false)
@@ -76,16 +71,12 @@ impl NostrRegistry {
}); });
// Construct the nostr client // Construct the nostr client
let client = Arc::new( let path = config_dir().join("nostr");
ClientBuilder::default() let lmdb = NostrLMDB::open(path).expect("Failed to initialize database");
.gossip(gossip) let client = ClientBuilder::default().database(lmdb).opts(opts).build();
.database(lmdb)
.opts(opts)
.build(),
);
let tracker = Arc::new(RwLock::new(EventTracker::default())); let tracker = Arc::new(RwLock::new(EventTracker::default()));
let cache_manager = Arc::new(RwLock::new(CacheManager::default())); let gossip = Arc::new(RwLock::new(Gossip::default()));
let mut tasks = smallvec![]; let mut tasks = smallvec![];
@@ -94,8 +85,8 @@ impl NostrRegistry {
// //
// And handle notifications from the nostr relay pool channel // And handle notifications from the nostr relay pool channel
cx.background_spawn({ cx.background_spawn({
let client = Arc::clone(&client); let client = client.clone();
let cache_manager = Arc::clone(&cache_manager); let gossip = Arc::clone(&gossip);
let tracker = Arc::clone(&tracker); let tracker = Arc::clone(&tracker);
let _ = initialized_at(); let _ = initialized_at();
@@ -104,7 +95,7 @@ impl NostrRegistry {
Self::connect(&client).await; Self::connect(&client).await;
// Handle notifications from the relay pool // Handle notifications from the relay pool
Self::handle_notifications(&client, &cache_manager, &tracker).await; Self::handle_notifications(&client, &gossip, &tracker).await;
} }
}), }),
); );
@@ -112,7 +103,7 @@ impl NostrRegistry {
Self { Self {
client, client,
tracker, tracker,
cache_manager, gossip,
_tasks: tasks, _tasks: tasks,
} }
} }
@@ -135,12 +126,10 @@ impl NostrRegistry {
async fn handle_notifications( async fn handle_notifications(
client: &Client, client: &Client,
cache: &Arc<RwLock<CacheManager>>, gossip: &Arc<RwLock<Gossip>>,
tracker: &Arc<RwLock<EventTracker>>, tracker: &Arc<RwLock<EventTracker>>,
) { ) {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
log::info!("Listening for notifications");
let mut processed_events = HashSet::new(); let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await { while let Ok(notification) = notifications.recv().await {
@@ -158,53 +147,50 @@ impl NostrRegistry {
match event.kind { match event.kind {
Kind::RelayList => { Kind::RelayList => {
if Self::is_self_authored(client, &event).await { let mut gossip = gossip.write().await;
log::info!("Found relay list event for the current user"); gossip.insert_relays(&event);
let author = event.pubkey;
let announcement = Kind::Custom(10044); let urls: Vec<RelayUrl> = Self::extract_write_relays(&event);
let author = event.pubkey;
// Fetch user's messaging relays event
_ = Self::subscribe(client, author, Kind::InboxRelays).await;
// Fetch user's encryption announcement event // Fetch user's encryption announcement event
_ = Self::subscribe(client, author, announcement).await; Self::get(client, &urls, author, Kind::Custom(10044)).await;
// Fetch user's messaging relays event
Self::get(client, &urls, author, Kind::InboxRelays).await;
// Verify if the event is belonging to the current user
if Self::is_self_authored(client, &event).await {
// Fetch user's metadata event // Fetch user's metadata event
_ = Self::subscribe(client, author, Kind::Metadata).await; Self::get(client, &urls, author, Kind::Metadata).await;
// Fetch user's contact list event // Fetch user's contact list event
_ = Self::subscribe(client, author, Kind::ContactList).await; Self::get(client, &urls, author, Kind::ContactList).await;
} }
} }
Kind::InboxRelays => { Kind::InboxRelays => {
// Extract up to 3 messaging relays let mut gossip = gossip.write().await;
let urls: Vec<RelayUrl> = gossip.insert_messaging_relays(&event);
nip17::extract_relay_list(&event).take(3).cloned().collect();
// Subscribe to gift wrap events if event is from current user
if Self::is_self_authored(client, &event).await { if Self::is_self_authored(client, &event).await {
log::info!("Found messaging list event for the current user"); // Extract user's messaging relays
let urls: Vec<RelayUrl> =
nip17::extract_relay_list(&event).cloned().collect();
if let Err(e) = // Fetch user's inbox messages in the extracted relays
Self::get_messages(client, &urls, event.pubkey).await Self::get_messages(client, event.pubkey, &urls).await;
{
log::error!("Failed to subscribe to gift wrap events: {e}");
} }
} }
// Cache the messaging relays
let mut cache = cache.write().await;
cache.insert_relay(event.pubkey, urls);
}
Kind::Custom(10044) => { Kind::Custom(10044) => {
// Cache the announcement event let mut gossip = gossip.write().await;
if let Ok(announcement) = Self::extract_announcement(&event) { gossip.insert_announcement(&event);
let mut cache = cache.write().await;
cache.insert_announcement(event.pubkey, Some(announcement));
}
} }
Kind::ContactList => { Kind::ContactList => {
if Self::is_self_authored(client, &event).await { if Self::is_self_authored(client, &event).await {
let pubkeys: Vec<_> = event.tags.public_keys().copied().collect(); let public_keys: Vec<PublicKey> =
event.tags.public_keys().copied().collect();
if let Err(e) = Self::get_metadata_for_list(client, pubkeys).await { if let Err(e) =
Self::get_metadata_for_list(client, public_keys).await
{
log::error!("Failed to get metadata for list: {e}"); log::error!("Failed to get metadata for list: {e}");
} }
} }
@@ -242,47 +228,59 @@ impl NostrRegistry {
false false
} }
/// Subscribe for events that match the given kind for a given author /// Get event that match the given kind for a given author
async fn subscribe(client: &Client, author: PublicKey, kind: Kind) -> Result<(), Error> { async fn get(client: &Client, urls: &[RelayUrl], author: PublicKey, kind: Kind) {
// Skip if no relays are provided
if urls.is_empty() {
return;
}
// Ensure relay connections
for url in urls.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new().author(author).kind(kind).limit(1); let filter = Filter::new().author(author).kind(kind).limit(1);
// Subscribe to filters from the user's write relays // Subscribe to filters from the user's write relays
client.subscribe(filter, Some(opts)).await?; if let Err(e) = client.subscribe_to(urls, filter, Some(opts)).await {
log::error!("Failed to subscribe: {}", e);
Ok(()) }
} }
/// Get all gift wrap events in the messaging relays for a given public key /// Get all gift wrap events in the messaging relays for a given public key
async fn get_messages( pub async fn get_messages(client: &Client, public_key: PublicKey, urls: &[RelayUrl]) {
client: &Client, // Verify that there are relays provided
urls: &[RelayUrl], if urls.is_empty() {
public_key: PublicKey, return;
) -> Result<(), Error> { }
// Ensure relay connection
for url in urls.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION); let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
// Verify that there are relays provided // Unsubscribe from the previous subscription
if urls.is_empty() { client.unsubscribe(&id).await;
return Err(anyhow!("No relays provided"));
}
// Add and connect relays
for url in urls {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Subscribe to filters to user's messaging relays // Subscribe to filters to user's messaging relays
client.subscribe_with_id_to(urls, id, filter, None).await?; if let Err(e) = client.subscribe_with_id_to(urls, id, filter, None).await {
log::error!("Failed to subscribe: {}", e);
Ok(()) } else {
log::info!("Subscribed to gift wrap events for public key {public_key}",);
}
} }
/// Get metadata for a list of public keys /// Get metadata for a list of public keys
async fn get_metadata_for_list(client: &Client, pubkeys: Vec<PublicKey>) -> Result<(), Error> { async fn get_metadata_for_list(client: &Client, pubkeys: Vec<PublicKey>) -> Result<(), Error> {
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList];
// Return if the list is empty // Return if the list is empty
if pubkeys.is_empty() { if pubkeys.is_empty() {
@@ -290,7 +288,7 @@ impl NostrRegistry {
} }
let filter = Filter::new() let filter = Filter::new()
.limit(pubkeys.len() * kinds.len() + 10) .limit(pubkeys.len() * kinds.len())
.authors(pubkeys) .authors(pubkeys)
.kinds(kinds); .kinds(kinds);
@@ -302,6 +300,19 @@ impl NostrRegistry {
Ok(()) Ok(())
} }
fn extract_write_relays(event: &Event) -> Vec<RelayUrl> {
nip65::extract_relay_list(event)
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url.to_owned())
} else {
None
}
})
.take(3)
.collect()
}
/// Extract an encryption keys announcement from an event. /// Extract an encryption keys announcement from an event.
pub fn extract_announcement(event: &Event) -> Result<Announcement, Error> { pub fn extract_announcement(event: &Event) -> Result<Announcement, Error> {
let public_key = event let public_key = event
@@ -342,8 +353,8 @@ impl NostrRegistry {
} }
/// Returns a reference to the nostr client. /// Returns a reference to the nostr client.
pub fn client(&self) -> Arc<Client> { pub fn client(&self) -> Client {
Arc::clone(&self.client) self.client.clone()
} }
/// Returns a reference to the event tracker. /// Returns a reference to the event tracker.
@@ -352,7 +363,7 @@ impl NostrRegistry {
} }
/// Returns a reference to the cache manager. /// Returns a reference to the cache manager.
pub fn cache_manager(&self) -> Arc<RwLock<CacheManager>> { pub fn gossip(&self) -> Arc<RwLock<Gossip>> {
Arc::clone(&self.cache_manager) Arc::clone(&self.gossip)
} }
} }

View File

@@ -3,6 +3,8 @@ use std::collections::{HashMap, HashSet};
use gpui::SharedString; use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use crate::NostrRegistry;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Announcement { pub struct Announcement {
id: EventId, id: EventId,
@@ -56,32 +58,129 @@ impl Response {
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct CacheManager { pub struct Gossip {
/// Cache of messaging relays for each public key /// Gossip relays for each public key
relay: HashMap<PublicKey, HashSet<RelayUrl>>, relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
/// Cache of device announcement for each public key /// Messaging relays for each public key
announcement: HashMap<PublicKey, Option<Announcement>>, messaging_relays: HashMap<PublicKey, HashSet<RelayUrl>>,
/// Encryption announcement for each public key
announcements: HashMap<PublicKey, Option<Announcement>>,
} }
impl CacheManager { impl Gossip {
pub fn relay(&self, public_key: &PublicKey) -> Option<&HashSet<RelayUrl>> { /// Get inbox relays for a public key
self.relay.get(public_key) pub fn inbox_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Write) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
} }
pub fn insert_relay(&mut self, public_key: PublicKey, urls: Vec<RelayUrl>) { /// Get outbox relays for a public key
self.relay.entry(public_key).or_default().extend(urls); pub fn outbox_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.relays
.get(public_key)
.map(|relays| {
relays
.iter()
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
} }
pub fn announcement(&self, public_key: &PublicKey) -> Option<&Option<Announcement>> { /// Insert gossip relays for a public key
self.announcement.get(public_key) pub fn insert_relays(&mut self, event: &Event) {
self.relays.entry(event.pubkey).or_default().extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::RelayMetadata {
relay_url,
metadata,
}) = tag.clone().to_standardized()
{
Some((relay_url, metadata))
} else {
None
}
})
.take(3),
);
} }
pub fn insert_announcement( /// Get messaging relays for a public key
&mut self, pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
public_key: PublicKey, self.messaging_relays
announcement: Option<Announcement>, .get(public_key)
) { .cloned()
self.announcement.insert(public_key, announcement); .unwrap_or_default()
.into_iter()
.collect()
}
/// Insert messaging relays for a public key
pub fn insert_messaging_relays(&mut self, event: &Event) {
self.messaging_relays
.entry(event.pubkey)
.or_default()
.extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::Relay(url)) = tag.as_standardized() {
Some(url.to_owned())
} else {
None
}
})
.take(3),
);
}
/// Ensure connections for the given relay list
pub async fn ensure_connections(&self, client: &Client, urls: &[RelayUrl]) {
for url in urls {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
}
/// Get announcement for a public key
pub fn announcement(&self, public_key: &PublicKey) -> Option<Announcement> {
self.announcements
.get(public_key)
.cloned()
.unwrap_or_default()
}
/// Insert announcement for a public key
pub fn insert_announcement(&mut self, event: &Event) {
let announcement = NostrRegistry::extract_announcement(event).ok();
self.announcements
.entry(event.pubkey)
.or_insert(announcement);
} }
} }

View File

@@ -11,9 +11,6 @@ pub fn initialized_at() -> &'static Timestamp {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct EventTracker { pub struct EventTracker {
/// Tracking events that have failed to unwrap
pub failed_unwrap_events: Vec<Event>,
/// Tracking events that have been resent by Coop in the current session /// Tracking events that have been resent by Coop in the current session
pub resent_ids: Vec<Output<EventId>>, pub resent_ids: Vec<Output<EventId>>,
@@ -28,10 +25,6 @@ pub struct EventTracker {
} }
impl EventTracker { impl EventTracker {
pub fn failed_unwrap_events(&self) -> &Vec<Event> {
&self.failed_unwrap_events
}
pub fn resent_ids(&self) -> &Vec<Output<EventId>> { pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
&self.resent_ids &self.resent_ids
} }