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

View File

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

View File

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

View File

@@ -18,8 +18,7 @@ use nostr_sdk::prelude::*;
pub use room::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use smol::lock::RwLock;
use state::{initialized_at, EventTracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use state::{initialized_at, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
mod message;
mod room;
@@ -41,6 +40,9 @@ pub struct ChatRegistry {
/// Loading status of the registry
pub loading: bool,
/// Async task for handling notifications
handle_notifications: Task<()>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
@@ -82,11 +84,19 @@ impl ChatRegistry {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let tracker = nostr.read(cx).tracker();
let status = Arc::new(AtomicBool::new(true));
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 tasks = smallvec![];
@@ -98,29 +108,27 @@ impl ChatRegistry {
move |this, state, cx| {
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();
let status = Arc::clone(&status);
let tx = tx.clone();
let signer = Some(signer);
async move {
Self::handle_notifications(&client, &signer, &tx, &status).await
}
});
cx.notify();
}
}
}),
);
tasks.push(
// Handle notifications
cx.background_spawn({
let client = Arc::clone(&client);
let status = Arc::clone(&status);
let tx = tx.clone();
async move { Self::handle_notifications(&client, &tracker, &tx, &status).await }
}),
);
tasks.push(
// Handle unwrapping status
cx.background_spawn({
let client = Arc::clone(&client);
async move { Self::handle_unwrapping(&client, &status, &tx).await }
}),
cx.background_spawn(
async move { Self::handle_unwrapping(&client, &status, &tx).await },
),
);
tasks.push(
@@ -155,23 +163,24 @@ impl ChatRegistry {
Self {
rooms: vec![],
loading: true,
handle_notifications,
_subscriptions: subscriptions,
_tasks: tasks,
}
}
async fn handle_notifications(
async fn handle_notifications<T>(
client: &Client,
tracker: &Arc<RwLock<EventTracker>>,
signer: &Option<T>,
tx: &Sender<Signal>,
status: &Arc<AtomicBool>,
) {
let mut notifications = client.notifications();
log::info!("Listening for notifications");
) where
T: NostrSigner,
{
let initialized_at = initialized_at();
let subscription_id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
let mut notifications = client.notifications();
let mut public_keys = HashSet::new();
let mut processed_events = HashSet::new();
@@ -194,7 +203,7 @@ impl ChatRegistry {
}
// 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) => {
// Get all public keys
public_keys.extend(rumor.all_pubkeys());
@@ -209,7 +218,7 @@ impl ChatRegistry {
Self::get_metadata(client, public_keys).await.ok();
}
match &event.created_at >= initialized_at {
match &rumor.created_at >= initialized_at {
true => {
let new_message = NewMessage::new(event.id, rumor);
let signal = Signal::Message(new_message);
@@ -223,11 +232,8 @@ impl ChatRegistry {
}
}
}
Err(_e) => {
let mut tracker = tracker.write().await;
tracker.failed_unwrap_events.push(event.as_ref().clone());
drop(tracker);
Err(e) => {
log::warn!("Failed to unwrap gift wrap event: {}", e);
}
}
}
@@ -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
pub fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading;
@@ -600,14 +566,21 @@ impl ChatRegistry {
}
// 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
if let Ok(event) = Self::get_rumor(client, gift_wrap.id).await {
return Ok(event);
}
// Try to unwrap with the available signer
let unwrapped = Self::try_unwrap(client, gift_wrap).await?;
let unwrapped = Self::try_unwrap(client, signer, gift_wrap).await?;
let mut rumor_unsigned = unwrapped.rumor;
// 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
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 unwrapped = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?;
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
async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Result<(), Error> {
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 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
if authors.is_empty() {
@@ -726,7 +710,7 @@ impl ChatRegistry {
}
let filter = Filter::new()
.limit(authors.len() * kinds.len() + 10)
.limit(authors.len() * kinds.len())
.authors(authors)
.kinds(kinds);

View File

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

View File

@@ -272,13 +272,13 @@ impl ChatPanel {
// Get the current room entity
let room = self.room.read(cx);
let opts = self.options.read(cx);
// 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 opts = self.options.read(cx);
let send_message = room.send_message(&rumor, opts, cx);
// Optimistically update message list

View File

@@ -197,7 +197,7 @@ impl Compose {
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
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);
client

View File

@@ -175,9 +175,6 @@ impl EditProfile {
}
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 name = self.name_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);
}
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
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;
// Sign the new metadata event
let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?;
// Send event to user's write relayss
client.send_event(&event).await?;
client.send_event_to(write_relays, &event).await?;
// Return the updated profile
let metadata = Metadata::from_json(&event.content).unwrap_or_default();

View File

@@ -137,11 +137,12 @@ impl NewAccount {
// Verify the signer
let signer = client.signer().await?;
let nip65_relays = default_nip65_relays();
// Construct a NIP-65 event
let event = EventBuilder::new(Kind::RelayList, "")
.tags(
default_nip65_relays()
nip65_relays
.iter()
.cloned()
.map(|(url, metadata)| Tag::relay_metadata(url, metadata)),
@@ -152,6 +153,25 @@ impl NewAccount {
// Set NIP-65 relays
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
let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(default_nip17_relays().iter().cloned().map(Tag::relay))
@@ -159,13 +179,13 @@ impl NewAccount {
.await?;
// Set NIP-17 relays
client.send_event(&event).await?;
client.send_event_to(&write_relays, &event).await?;
// Construct a metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
// Set metadata
client.send_event(&event).await?;
client.send_event_to(&write_relays, &event).await?;
Ok(())
});

View File

@@ -1,4 +1,3 @@
use std::sync::Arc;
use std::time::Duration;
use common::{nip05_verify, shorten_pubkey, RenderedProfile, RenderedTimestamp, BOOTSTRAP_RELAYS};
@@ -44,7 +43,7 @@ impl Screening {
let mut tasks = smallvec![];
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 {
let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?;

View File

@@ -155,10 +155,17 @@ impl SetupRelay {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let relays = self.relays.clone();
let task: Task<Result<(), Error>> = 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 tags: Vec<Tag> = relays
.iter()
@@ -171,7 +178,7 @@ impl SetupRelay {
.await?;
// Set messaging relays
client.send_event(&event).await?;
client.send_event_to(write_relays, &event).await?;
// Connect to messaging relays
for relay in relays.iter() {

View File

@@ -137,15 +137,13 @@ impl Sidebar {
async fn request_metadata(client: &Client, public_key: PublicKey) -> Result<(), Error> {
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);
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
log::info!("Subscribe to get metadata for: {public_key}");
Ok(())
}

View File

@@ -49,10 +49,10 @@ pub struct Encryption {
handle_requests: Option<Task<()>>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
_subscriptions: SmallVec<[Subscription; 2]>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 2]>,
_tasks: SmallVec<[Task<()>; 1]>,
}
impl Encryption {
@@ -69,48 +69,36 @@ impl Encryption {
/// Create a new encryption instance
fn new(cx: &mut Context<Self>) -> Self {
let account = Account::global(cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let requests = cx.new(|_| HashSet::default());
let encryption = cx.new(|_| None);
let client_signer = cx.new(|_| None);
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
subscriptions.push(
// Observe the account state
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);
}
}),
);
tasks.push(
// Get the client key
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");
}
subscriptions.push(
// Observe the encryption signer state
cx.observe(&encryption, |this, state, cx| {
if state.read(cx).is_some() {
this._tasks.push(this.resubscribe_messages(cx));
}
}),
);
@@ -123,7 +111,7 @@ impl Encryption {
handle_notifications: None,
handle_requests: None,
_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>) {
let task = self._get_announcement(cx);
let delay = Duration::from_secs(10);
self._tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(5)).await;
self._tasks.push(
// Run task in the background
cx.spawn(async move |this, cx| {
cx.background_executor().timer(delay).await;
match task.await {
Ok(announcement) => {
if let Ok(announcement) = task.await {
this.update(cx, |this, cx| {
this.load_encryption(&announcement, cx);
// Set the announcement
@@ -190,11 +214,8 @@ impl Encryption {
})
.expect("Entity has been released");
}
Err(err) => {
log::error!("Failed to get announcement: {}", err);
}
};
}));
}),
);
}
fn _get_announcement(&self, cx: &App) -> Task<Result<Announcement, Error>> {
@@ -236,12 +257,65 @@ impl Encryption {
this.listen_request(cx);
}
}
this.load_response(cx);
})
.expect("Entity has been released");
})
.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
///
/// 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 task: Task<Result<(), Error>> = cx.background_spawn({
let client = Arc::clone(&client);
let client = nostr.read(cx).client();
async move {
let signer = client.signer().await?;
@@ -325,6 +399,7 @@ impl Encryption {
pub fn new_encryption(&self, cx: &App) -> Task<Result<Keys, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let keys = Keys::generate();
let public_key = keys.public_key();
@@ -336,6 +411,12 @@ impl Encryption {
Self::set_keys(&client, "encryption", secret).await?;
let signer = client.signer().await?;
let signer_pubkey = signer.get_public_key().await?;
let gossip = gossip.read().await;
let write_relays = gossip.inbox_relays(&signer_pubkey);
// Ensure connections to the write relays
gossip.ensure_connections(&client, &write_relays).await;
// Construct the announcement event
let event = EventBuilder::new(Kind::Custom(10044), "")
@@ -343,11 +424,12 @@ impl Encryption {
Tag::client(app_name()),
Tag::custom(TagKind::custom("n"), vec![public_key]),
])
.build(signer_pubkey)
.sign(&signer)
.await?;
// Send the announcement event to user's relays
client.send_event(&event).await?;
client.send_event_to(write_relays, &event).await?;
Ok(keys)
})
@@ -359,6 +441,7 @@ impl Encryption {
pub fn send_request(&self, cx: &App) -> Task<Result<Option<Keys>, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
// Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else {
@@ -395,6 +478,12 @@ impl Encryption {
Ok(Some(keys))
}
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
let event = EventBuilder::new(Kind::Custom(4454), "")
.tags(vec![
@@ -405,7 +494,7 @@ impl Encryption {
.await?;
// 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
let subscription_id = SubscriptionId::new("listen-response");
@@ -413,12 +502,11 @@ impl Encryption {
let filter = Filter::new()
.kind(Kind::Custom(4455))
.author(public_key)
.pubkey(client_pubkey)
.since(Timestamp::now());
// Subscribe to the approval response event
client
.subscribe_with_id(subscription_id, filter, None)
.subscribe_with_id_to(&write_relays, subscription_id, filter, None)
.await?;
Ok(None)
@@ -433,6 +521,7 @@ impl Encryption {
pub fn send_response(&self, target: PublicKey, cx: &App) -> Task<Result<(), Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
// Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else {
@@ -440,6 +529,14 @@ impl Encryption {
};
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 client_pubkey = client_signer.get_public_key().await?;
@@ -457,30 +554,12 @@ impl Encryption {
Tag::custom(TagKind::custom("P"), vec![client_pubkey]),
Tag::public_key(target),
])
.sign(&client_signer)
.build(public_key)
.sign(&signer)
.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
client.send_event_to(urls, &event).await?;
client.send_event_to(write_relays, &event).await?;
Ok(())
})
@@ -493,12 +572,14 @@ impl Encryption {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let client_signer = self.client_signer.read(cx).clone().unwrap();
let mut processed_events = HashSet::new();
// Get the client signer
let Some(client_signer) = self.client_signer.read(cx).clone() else {
return Task::ready(Err(anyhow!("Client Signer is required")));
};
cx.background_spawn(async move {
let mut notifications = client.notifications();
log::info!("Listening for notifications");
let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else {
@@ -513,7 +594,7 @@ impl Encryption {
}
if event.kind != Kind::Custom(4455) {
// Skip non-gift wrap events
// Skip non-response events
continue;
}
@@ -528,6 +609,8 @@ impl Encryption {
let keys = Keys::new(secret);
return Ok(keys);
} else {
log::error!("Failed to extract response from event");
}
}
}
@@ -579,4 +662,21 @@ impl Encryption {
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::time::Duration;
@@ -181,6 +183,7 @@ impl EncryptionPanel {
});
}
Err(e) => {
this.set_requesting(false, 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>) {
let client_name = SharedString::from(req.client().to_string());
let target = req.public_key();
let id = SharedString::from(req.id().to_hex());
let loading = Rc::new(Cell::new(false));
let note = Notification::new()
.custom_id(SharedString::from(req.id().to_hex()))
.custom_id(id.clone())
.autohide(false)
.icon(IconName::Info)
.icon(IconName::Encryption)
.title(SharedString::from("Encryption Key Request"))
.content(move |_window, cx| {
v_flex()
@@ -208,14 +213,24 @@ impl EncryptionPanel {
))
.child(
v_flex()
.py_1()
.px_1p5()
.rounded_sm()
.text_xs()
.h_12()
.items_center()
.justify_center()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(client_name.clone()),
)
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(SharedString::from(target.to_hex())),
)
.into_any_element()
})
.action(move |_window, _cx| {
@@ -223,13 +238,38 @@ impl EncryptionPanel {
.label("Approve")
.small()
.primary()
.loading(false)
.disabled(false)
.on_click(move |_ev, _window, cx| {
let encryption = Encryption::global(cx);
let send_response = encryption.read(cx).send_response(target, cx);
.loading(loading.get())
.disabled(loading.get())
.on_click({
let loading = Rc::clone(&loading);
let id = id.clone();
send_response.detach();
move |_ev, window, cx| {
// Set loading state to true
loading.set(true);
let encryption = Encryption::global(cx);
let send_response = encryption.read(cx).send_response(target, cx);
let id = id.clone();
window
.spawn(cx, async move |cx| {
let result = send_response.await;
cx.update(|window, cx| {
match result {
Ok(_) => {
window.clear_notification_by_id(id, cx);
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.expect("Entity has been released");
})
.detach();
}
})
});
@@ -252,6 +292,7 @@ impl Render for EncryptionPanel {
const WARNING: &str = "Encryption Key is still in the alpha stage. Please be cautious.";
let encryption = Encryption::global(cx);
let announcement = encryption.read(cx).announcement();
let has_encryption = encryption.read(cx).has_encryption(cx);
v_flex()
@@ -259,149 +300,165 @@ impl Render for EncryptionPanel {
.max_w(px(340.))
.w(px(340.))
.text_sm()
.when(has_encryption, |this| {
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 name = announcement.client();
.when_some(announcement.as_ref(), |this, announcement| {
let pubkey = shorten_pubkey(announcement.public_key(), 16);
let name = announcement.client();
this.child(
v_flex()
.gap_2()
.child(div().font_semibold().child(SharedString::from(NOTICE)))
.child(
v_flex()
.h_12()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(cx.theme().warning_background)
.text_color(cx.theme().warning_foreground)
.child(name),
this.child(
v_flex()
.gap_2()
.when(has_encryption, |this| {
this.child(
h_flex()
.gap_1p5()
.text_sm()
.font_semibold()
.child(
Icon::new(IconName::CheckCircle)
.text_color(cx.theme().element_foreground)
.small(),
)
.child(SharedString::from("Encryption Key has been set")),
)
.child(
})
.when(!has_encryption, |this| {
this.child(div().font_semibold().child(SharedString::from(NOTICE)))
})
.child(
v_flex()
.gap_1()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Client Name:")),
)
.child(
h_flex()
.h_12()
.items_center()
.justify_center()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(name),
),
)
.child(
v_flex()
.gap_1()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Client Public Key:")),
)
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(SharedString::from(pubkey)),
),
)
.when(!has_encryption, |this| {
this.child(
v_flex()
.gap_1()
.gap_2()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from("Client Public Key:")),
.child(SharedString::from(SUGGEST)),
)
.child(
h_flex()
.h_7()
.w_full()
.px_2()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.child(SharedString::from(pubkey)),
.mt_2()
.gap_1()
.when(!self.requesting, |this| {
this.child(
Button::new("reset")
.label("Reset")
.flex_1()
.small()
.ghost_alt()
.loading(self.creating)
.disabled(self.creating)
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.new_encryption(window, cx);
},
)),
)
})
.when(!self.creating, |this| {
this.child(
Button::new("request")
.label({
if self.requesting {
"Wait for approval"
} else {
"Request"
}
})
.flex_1()
.small()
.primary()
.loading(self.requesting)
.disabled(self.requesting)
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.request(window, cx);
},
)),
)
}),
),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(SharedString::from(SUGGEST)),
)
.child(
h_flex()
.mt_2()
.gap_1()
.when(!self.requesting, |this| {
this.child(
Button::new("reset")
.label("Reset")
.flex_1()
.small()
.ghost_alt()
.loading(self.creating)
.disabled(self.creating)
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.new_encryption(window, cx);
},
)),
)
})
.when(!self.creating, |this| {
this.child(
Button::new("request")
.label({
if self.requesting {
"Wait for approval"
} else {
"Request"
}
})
.flex_1()
.small()
.primary()
.loading(self.requesting)
.disabled(self.requesting)
.on_click(cx.listener(
move |this, _ev, window, cx| {
this.request(window, cx);
},
)),
)
}),
)
.when_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 {
this.child(
v_flex()
.gap_2()
.child(
div()
.font_semibold()
.child(SharedString::from("Set up Encryption Key")),
)
.child(SharedString::from(DESCRIPTION))
.child(
div()
.text_xs()
.text_color(cx.theme().warning_foreground)
.child(SharedString::from(WARNING)),
)
.child(
Button::new("create")
.label("Setup")
.flex_1()
.small()
.primary()
.loading(self.creating)
.disabled(self.creating)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.new_encryption(window, cx);
})),
),
)
}
}),
)
})
.when_none(&announcement, |this| {
this.child(
v_flex()
.gap_2()
.child(
div()
.font_semibold()
.child(SharedString::from("Set up Encryption Key")),
)
.child(SharedString::from(DESCRIPTION))
.child(
div()
.text_xs()
.text_color(cx.theme().warning_foreground)
.child(SharedString::from(WARNING)),
)
.child(
Button::new("create")
.label("Setup")
.flex_1()
.small()
.primary()
.loading(self.creating)
.disabled(self.creating)
.on_click(cx.listener(move |this, _ev, window, cx| {
this.new_encryption(window, cx);
})),
),
)
})
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_xs()
.text_center()
.text_color(cx.theme().danger_foreground)
.child(error.clone()),
)
})
}
}

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ use std::collections::{HashMap, HashSet};
use gpui::SharedString;
use nostr_sdk::prelude::*;
use crate::NostrRegistry;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Announcement {
id: EventId,
@@ -56,32 +58,129 @@ impl Response {
}
#[derive(Debug, Clone, Default)]
pub struct CacheManager {
/// Cache of messaging relays for each public key
relay: HashMap<PublicKey, HashSet<RelayUrl>>,
pub struct Gossip {
/// Gossip relays for each public key
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
/// Cache of device announcement for each public key
announcement: HashMap<PublicKey, Option<Announcement>>,
/// Messaging relays for each public key
messaging_relays: HashMap<PublicKey, HashSet<RelayUrl>>,
/// Encryption announcement for each public key
announcements: HashMap<PublicKey, Option<Announcement>>,
}
impl CacheManager {
pub fn relay(&self, public_key: &PublicKey) -> Option<&HashSet<RelayUrl>> {
self.relay.get(public_key)
impl Gossip {
/// Get inbox relays for a 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>) {
self.relay.entry(public_key).or_default().extend(urls);
/// Get outbox relays for a public key
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>> {
self.announcement.get(public_key)
/// Insert gossip relays for a public key
pub fn insert_relays(&mut self, event: &Event) {
self.relays.entry(event.pubkey).or_default().extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::RelayMetadata {
relay_url,
metadata,
}) = tag.clone().to_standardized()
{
Some((relay_url, metadata))
} else {
None
}
})
.take(3),
);
}
pub fn insert_announcement(
&mut self,
public_key: PublicKey,
announcement: Option<Announcement>,
) {
self.announcement.insert(public_key, announcement);
/// Get messaging relays for a public key
pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
self.messaging_relays
.get(public_key)
.cloned()
.unwrap_or_default()
.into_iter()
.collect()
}
/// Insert messaging relays for a public key
pub fn insert_messaging_relays(&mut self, event: &Event) {
self.messaging_relays
.entry(event.pubkey)
.or_default()
.extend(
event
.tags
.iter()
.filter_map(|tag| {
if let Some(TagStandard::Relay(url)) = tag.as_standardized() {
Some(url.to_owned())
} else {
None
}
})
.take(3),
);
}
/// 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)]
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
pub resent_ids: Vec<Output<EventId>>,
@@ -28,10 +25,6 @@ pub struct EventTracker {
}
impl EventTracker {
pub fn failed_unwrap_events(&self) -> &Vec<Event> {
&self.failed_unwrap_events
}
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
&self.resent_ids
}