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

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
}