chore: improve gossip implementation (#184)

* add send event function

* add set nip17 and set nip65 functions

* setup gossip relays

* .
This commit is contained in:
reya
2025-10-12 20:22:57 +07:00
committed by GitHub
parent 7fc727461e
commit 2415374567
13 changed files with 656 additions and 295 deletions

View File

@@ -19,17 +19,6 @@ pub const BOOTSTRAP_RELAYS: [&str; 5] = [
/// Search Relays.
pub const SEARCH_RELAYS: [&str; 1] = ["wss://relay.nostr.band"];
/// NIP65 Relays. Used for new account
pub const NIP65_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.net",
"wss://nos.lol",
];
/// Messaging Relays. Used for new account
pub const NIP17_RELAYS: [&str; 2] = ["wss://nip17.com", "wss://auth.nostr1.com"];
/// Default relay for Nostr Connect
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";

View File

@@ -13,6 +13,8 @@ pub mod state;
static APP_STATE: OnceLock<AppState> = OnceLock::new();
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
static NIP65_RELAYS: OnceLock<Vec<(RelayUrl, Option<RelayMetadata>)>> = OnceLock::new();
static NIP17_RELAYS: OnceLock<Vec<RelayUrl>> = OnceLock::new();
/// Initialize the application state.
pub fn app_state() -> &'static AppState {
@@ -42,3 +44,39 @@ pub fn nostr_client() -> &'static Client {
ClientBuilder::default().database(lmdb).opts(opts).build()
})
}
/// Default NIP65 Relays. Used for new account
pub fn default_nip65_relays() -> &'static Vec<(RelayUrl, Option<RelayMetadata>)> {
NIP65_RELAYS.get_or_init(|| {
vec![
(
RelayUrl::parse("wss://nostr.mom").unwrap(),
Some(RelayMetadata::Read),
),
(
RelayUrl::parse("wss://nostr.bitcoiner.social").unwrap(),
Some(RelayMetadata::Read),
),
(
RelayUrl::parse("wss://nostr.oxtr.dev").unwrap(),
Some(RelayMetadata::Write),
),
(
RelayUrl::parse("wss://nostr.fmt.wiz.biz").unwrap(),
Some(RelayMetadata::Write),
),
(RelayUrl::parse("wss://relay.primal.net").unwrap(), None),
(RelayUrl::parse("wss://relay.damus.io").unwrap(), None),
]
})
}
/// Default NIP17 Relays. Used for new account
pub fn default_nip17_relays() -> &'static Vec<RelayUrl> {
NIP17_RELAYS.get_or_init(|| {
vec![
RelayUrl::parse("wss://nip17.com").unwrap(),
RelayUrl::parse("wss://auth.nostr1.com").unwrap(),
]
})
}

View File

@@ -15,10 +15,12 @@ pub struct Gossip {
}
impl Gossip {
/// Parse and insert NIP-65 or NIP-17 relays into the gossip state.
pub fn insert(&mut self, event: &Event) {
match event.kind {
Kind::InboxRelays => {
let urls: Vec<RelayUrl> = nip17::extract_relay_list(event).cloned().collect();
let urls: Vec<RelayUrl> =
nip17::extract_relay_list(event).take(3).cloned().collect();
if !urls.is_empty() {
self.nip17.entry(event.pubkey).or_default().extend(urls);
@@ -37,6 +39,7 @@ impl Gossip {
}
}
/// Get all write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> {
self.nip65
.get(public_key)
@@ -51,6 +54,7 @@ impl Gossip {
.unwrap_or_default()
}
/// Get all read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> {
self.nip65
.get(public_key)
@@ -65,6 +69,7 @@ impl Gossip {
.unwrap_or_default()
}
/// Get all messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<&RelayUrl> {
self.nip17
.get(public_key)
@@ -72,17 +77,30 @@ impl Gossip {
.unwrap_or_default()
}
pub async fn get_nip65(&mut self, public_key: PublicKey) -> Result<(), Error> {
/// Get and verify NIP-65 relays for a given public key
///
/// Only fetch from the public relays
pub async fn get_nip65(&self, public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client();
let timeout = Duration::from_secs(5);
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new()
let latest_filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.limit(1);
// Subscribe to events from the bootstrapping relays
client
.subscribe_to(BOOTSTRAP_RELAYS, latest_filter.clone(), Some(opts))
.await?;
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.since(Timestamp::now());
// Continuously subscribe for new events from the bootstrap relays
client
.subscribe_to(BOOTSTRAP_RELAYS, filter.clone(), Some(opts))
.await?;
@@ -91,7 +109,7 @@ impl Gossip {
smol::spawn(async move {
smol::Timer::after(timeout).await;
if client.database().count(filter).await.unwrap_or(0) < 1 {
if client.database().count(latest_filter).await.unwrap_or(0) < 1 {
app_state()
.signal
.send(SignalKind::GossipRelaysNotFound)
@@ -103,16 +121,49 @@ impl Gossip {
Ok(())
}
pub async fn get_nip17(&mut self, public_key: PublicKey) -> Result<(), Error> {
/// Set NIP-65 relays for a current user
pub async fn set_nip65(
&mut self,
relays: &[(RelayUrl, Option<RelayMetadata>)],
) -> Result<(), Error> {
let client = nostr_client();
let signer = client.signer().await?;
let tags: Vec<Tag> = relays
.iter()
.map(|(url, metadata)| Tag::relay_metadata(url.to_owned(), metadata.to_owned()))
.collect();
let event = EventBuilder::new(Kind::RelayList, "")
.tags(tags)
.sign(&signer)
.await?;
// Send event to the public relays
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
// Update gossip data
for relay in relays {
self.nip65
.entry(event.pubkey)
.or_default()
.insert(relay.to_owned());
}
// Get NIP-17 relays
self.get_nip17(event.pubkey).await?;
Ok(())
}
/// Get and verify NIP-17 relays for a given public key
///
/// Only fetch from public key's write relays
pub async fn get_nip17(&self, public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client();
let timeout = Duration::from_secs(5);
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let urls = self.write_relays(&public_key);
// Ensure user's have at least one write relay
@@ -126,7 +177,22 @@ impl Gossip {
client.connect_relay(url).await?;
}
let latest_filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Subscribe to events from the bootstrapping relays
client
.subscribe_to(urls.clone(), latest_filter.clone(), Some(opts))
.await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.since(Timestamp::now());
// Continuously subscribe for new events from the bootstrap relays
client
.subscribe_to(urls, filter.clone(), Some(opts))
.await?;
@@ -135,7 +201,7 @@ impl Gossip {
smol::spawn(async move {
smol::Timer::after(timeout).await;
if client.database().count(filter).await.unwrap_or(0) < 1 {
if client.database().count(latest_filter).await.unwrap_or(0) < 1 {
app_state()
.signal
.send(SignalKind::MessagingRelaysNotFound)
@@ -147,7 +213,51 @@ impl Gossip {
Ok(())
}
pub async fn subscribe(&mut self, public_key: PublicKey, kind: Kind) -> Result<(), Error> {
/// Set NIP-17 relays for a current user
pub async fn set_nip17(&mut self, relays: &[RelayUrl]) -> Result<(), Error> {
let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let urls = self.write_relays(&public_key);
// Ensure user's have at least one relay
if urls.is_empty() {
return Err(anyhow!("Relays are empty"));
}
// Ensure connection to relays
for url in urls.iter().cloned() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(relays.iter().map(|relay| Tag::relay(relay.to_owned())))
.sign(&signer)
.await?;
// Send event to the public relays
client.send_event_to(urls, &event).await?;
// Update gossip data
for relay in relays {
self.nip17
.entry(event.pubkey)
.or_default()
.insert(relay.to_owned());
}
// Run inbox monitor
self.monitor_inbox(event.pubkey).await?;
Ok(())
}
/// Subscribe for events that match the given kind for a given author
///
/// Only fetch from author's write relays
pub async fn subscribe(&self, public_key: PublicKey, kind: Kind) -> Result<(), Error> {
let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
@@ -171,7 +281,10 @@ impl Gossip {
Ok(())
}
pub async fn bulk_subscribe(&mut self, public_keys: HashSet<PublicKey>) -> Result<(), Error> {
/// Bulk subscribe to metadata events for a list of public keys
///
/// Only fetch from the public relays
pub async fn bulk_subscribe(&self, public_keys: HashSet<PublicKey>) -> Result<(), Error> {
if public_keys.is_empty() {
return Err(anyhow!("You need at least one public key"));
}
@@ -192,7 +305,7 @@ impl Gossip {
}
/// Monitor all gift wrap events in the messaging relays for a given public key
pub async fn monitor_inbox(&mut self, public_key: PublicKey) -> Result<(), Error> {
pub async fn monitor_inbox(&self, public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client();
let id = SubscriptionId::new("inbox");
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
@@ -214,4 +327,27 @@ impl Gossip {
Ok(())
}
/// Send an event to author's write relays
pub async fn send_event_to_write_relays(&self, event: &Event) -> Result<(), Error> {
let client = nostr_client();
let public_key = event.pubkey;
let urls = self.write_relays(&public_key);
// Ensure user's have at least one relay
if urls.is_empty() {
return Err(anyhow!("Relays are empty"));
}
// Ensure connection to relays
for url in urls.iter().cloned() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Send event to relays
client.send_event(event).await?;
Ok(())
}
}

View File

@@ -15,7 +15,7 @@ use crate::nostr_client;
use crate::paths::support_dir;
use crate::state::gossip::Gossip;
pub mod gossip;
mod gossip;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct AuthRequest {
@@ -267,14 +267,18 @@ impl AppState {
match event.kind {
Kind::RelayList => {
let mut gossip = self.gossip.write().await;
let is_self_authored = Self::is_self_authored(&event).await;
// Update NIP-65 relays for event's public key
gossip.insert(&event);
{
let mut gossip = self.gossip.write().await;
gossip.insert(&event);
}
let is_self_authored = Self::is_self_authored(&event).await;
// Get events if relay list belongs to current user
if is_self_authored {
let gossip = self.gossip.read().await;
// Fetch user's metadata event
gossip.subscribe(event.pubkey, Kind::Metadata).await.ok();
@@ -286,16 +290,19 @@ impl AppState {
}
}
Kind::InboxRelays => {
let mut gossip = self.gossip.write().await;
let is_self_authored = Self::is_self_authored(&event).await;
// Update NIP-17 relays for event's public key
gossip.insert(&event);
{
let mut gossip = self.gossip.write().await;
gossip.insert(&event);
}
let is_self_authored = Self::is_self_authored(&event).await;
// Subscribe to gift wrap events if messaging relays belong to the current user
if is_self_authored {
if let Err(e) = gossip.monitor_inbox(event.pubkey).await {
log::error!("Error: {e}");
let gossip = self.gossip.read().await;
if gossip.monitor_inbox(event.pubkey).await.is_err() {
self.signal.send(SignalKind::MessagingRelaysNotFound).await;
}
}
@@ -304,11 +311,15 @@ impl AppState {
let is_self_authored = Self::is_self_authored(&event).await;
if is_self_authored {
let mut gossip = self.gossip.write().await;
let public_keys: HashSet<PublicKey> =
event.tags.public_keys().copied().collect();
gossip.bulk_subscribe(public_keys).await.ok();
self.gossip
.read()
.await
.bulk_subscribe(public_keys)
.await
.ok();
}
}
Kind::Metadata => {
@@ -395,16 +406,16 @@ impl AppState {
// Process the batch if it's full
if batch.len() >= METADATA_BATCH_LIMIT {
let mut gossip = self.gossip.write().await;
let gossip = self.gossip.read().await;
gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok();
}
}
BatchEvent::Timeout => {
let mut gossip = self.gossip.write().await;
let gossip = self.gossip.read().await;
gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok();
}
BatchEvent::Closed => {
let mut gossip = self.gossip.write().await;
let gossip = self.gossip.read().await;
gossip.bulk_subscribe(std::mem::take(&mut batch)).await.ok();
// Exit the current loop

View File

@@ -7,15 +7,15 @@ use std::time::Duration;
use anyhow::{anyhow, Error};
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
use app_state::state::{AuthRequest, SignalKind, UnwrappingStatus};
use app_state::{app_state, nostr_client};
use app_state::{app_state, default_nip17_relays, default_nip65_relays, nostr_client};
use auto_update::AutoUpdater;
use client_keys::ClientKeys;
use common::display::RenderedProfile;
use common::event::EventUtils;
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, px, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem, Context,
Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
deferred, div, px, relative, rems, App, AppContext, AsyncWindowContext, Axis, ClipboardItem,
Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
};
use i18n::{shared_t, t};
@@ -41,7 +41,7 @@ use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Root, Sizable, Sty
use crate::actions::{DarkMode, Logout, ReloadMetadata, Settings};
use crate::views::compose::compose_button;
use crate::views::setup_relay::setup_nip17_relay;
use crate::views::setup_relay::SetupRelay;
use crate::views::{
account, chat, login, new_account, onboarding, preferences, sidebar, user_profile, welcome,
};
@@ -61,22 +61,25 @@ pub fn new_account(window: &mut Window, cx: &mut App) {
}
pub struct ChatSpace {
// App's Title Bar
/// App's Title Bar
title_bar: Entity<TitleBar>,
// App's Dock Area
/// App's Dock Area
dock: Entity<DockArea>,
// All authentication requests
/// All authentication requests
auth_requests: Entity<HashMap<RelayUrl, AuthRequest>>,
// Local state to determine if the user has set up NIP-17 relays
nip17_relays: bool,
/// Local state to determine if the user has set up NIP-17 relays
nip17_ready: bool,
// All subscriptions for observing the app state
/// Local state to determine if the user has set up NIP-65 relays
nip65_ready: bool,
/// All subscriptions for observing the app state
_subscriptions: SmallVec<[Subscription; 4]>,
// All long running tasks
/// All long running tasks
_tasks: SmallVec<[Task<()>; 5]>,
}
@@ -203,7 +206,8 @@ impl ChatSpace {
dock,
title_bar,
auth_requests,
nip17_relays: true,
nip17_ready: true,
nip65_ready: true,
_subscriptions: subscriptions,
_tasks: tasks,
}
@@ -361,13 +365,14 @@ impl ChatSpace {
}
SignalKind::GossipRelaysNotFound => {
view.update(cx, |this, cx| {
this.set_required_relays(cx);
this.set_required_gossip_relays(cx);
this.render_setup_gossip_relays_modal(window, cx);
})
.ok();
}
SignalKind::MessagingRelaysNotFound => {
view.update(cx, |this, cx| {
this.set_required_relays(cx);
this.set_required_dm_relays(cx);
})
.ok();
}
@@ -639,8 +644,13 @@ impl ChatSpace {
});
}
fn set_required_relays(&mut self, cx: &mut Context<Self>) {
self.nip17_relays = false;
fn set_required_dm_relays(&mut self, cx: &mut Context<Self>) {
self.nip17_ready = false;
cx.notify();
}
fn set_required_gossip_relays(&mut self, cx: &mut Context<Self>) {
self.nip65_ready = false;
cx.notify();
}
@@ -789,6 +799,212 @@ impl ChatSpace {
window.push_notification(t!("common.copied"), cx);
}
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
let relays = default_nip65_relays();
window.open_modal(cx, move |this, _window, cx| {
this.overlay_closable(false)
.show_close(false)
.keyboard(false)
.confirm()
.button_props(
ModalButtonProps::default()
.cancel_text(t!("common.configure"))
.ok_text(t!("common.use_default")),
)
.title(shared_t!("mailbox.modal"))
.child(
v_flex()
.gap_2()
.text_sm()
.child(shared_t!("mailbox.description"))
.child(
v_flex()
.gap_1()
.text_xs()
.text_color(cx.theme().text_muted)
.child(shared_t!("mailbox.write_label"))
.child(shared_t!("mailbox.read_label")),
)
.child(
div()
.font_semibold()
.text_xs()
.child(shared_t!("common.default")),
)
.child(v_flex().gap_1().children({
let mut items = Vec::with_capacity(relays.len());
for (url, metadata) in relays {
items.push(
div()
.h_7()
.px_1p5()
.h_flex()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.text_sm()
.child(
div()
.line_height(relative(1.2))
.child(SharedString::from(url.to_string())),
)
.when_some(metadata.as_ref(), |this, metadata| {
this.child(
div()
.text_xs()
.font_semibold()
.line_height(relative(1.2))
.child(SharedString::from(
metadata.to_string(),
)),
)
}),
);
}
items
})),
)
.on_cancel(|_, _window, _cx| {
// TODO: add configure relays
// true to close the modal
true
})
.on_ok(|_, window, cx| {
window
.spawn(cx, async move |cx| {
let app_state = app_state();
let relays = default_nip65_relays();
let mut gossip = app_state.gossip.write().await;
let result = gossip.set_nip65(relays).await;
cx.update(|window, cx| {
match result {
Ok(_) => {
window.close_modal(cx);
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
// false to keep modal open
false
})
})
}
fn render_setup_dm_relays_modal(window: &mut Window, cx: &mut App) {
let relays = default_nip17_relays();
window.open_modal(cx, move |this, _window, cx| {
this.overlay_closable(false)
.show_close(false)
.keyboard(false)
.confirm()
.button_props(
ModalButtonProps::default()
.cancel_text(t!("common.configure"))
.ok_text(t!("common.use_default")),
)
.title(shared_t!("messaging.modal"))
.child(
v_flex()
.gap_2()
.text_sm()
.child(shared_t!("messaging.description"))
.child(
div()
.font_semibold()
.text_xs()
.child(shared_t!("common.default")),
)
.child(v_flex().gap_1().children({
let mut items = Vec::with_capacity(relays.len());
for url in relays {
items.push(
div()
.h_7()
.px_1p5()
.h_flex()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.text_sm()
.child(
div()
.line_height(relative(1.2))
.child(SharedString::from(url.to_string())),
),
);
}
items
})),
)
.on_cancel(|_, window, cx| {
let view = cx.new(|cx| SetupRelay::new(window, cx));
let weak_view = view.downgrade();
window.open_modal(cx, move |modal, _window, _cx| {
let weak_view = weak_view.clone();
modal
.confirm()
.title(shared_t!("relays.modal"))
.child(view.clone())
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
.on_ok(move |_, window, cx| {
weak_view
.update(cx, |this, cx| {
this.set_relays(window, cx);
})
.ok();
// true to close the modal
false
})
});
// true to close the modal
true
})
.on_ok(|_, window, cx| {
window
.spawn(cx, async move |cx| {
let app_state = app_state();
let relays = default_nip17_relays();
let mut gossip = app_state.gossip.write().await;
let result = gossip.set_nip17(relays).await;
cx.update(|window, cx| {
match result {
Ok(_) => {
window.close_modal(cx);
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
// false to keep modal open
false
})
})
}
fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) {
window.open_modal(cx, |this, _window, _cx| {
this.overlay_closable(false)
@@ -954,8 +1170,18 @@ impl ChatSpace {
})),
)
})
.when(!self.nip17_relays, |this| {
this.child(setup_nip17_relay(t!("relays.button")))
.when(!self.nip17_ready, |this| {
this.child(
Button::new("setup-relays-button")
.icon(IconName::Info)
.label(t!("messaging.button"))
.warning()
.xsmall()
.rounded()
.on_click(move |_ev, window, cx| {
Self::render_setup_dm_relays_modal(window, cx);
}),
)
})
.child(
Button::new("user")

View File

@@ -1,7 +1,8 @@
use std::str::FromStr;
use std::time::Duration;
use app_state::nostr_client;
use anyhow::Error;
use app_state::{app_state, nostr_client};
use common::nip96::nip96_upload;
use gpui::prelude::FluentBuilder;
use gpui::{
@@ -164,7 +165,7 @@ impl EditProfile {
.detach();
}
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Option<Profile>, Error>> {
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> {
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();
@@ -187,18 +188,23 @@ impl EditProfile {
}
cx.background_spawn(async move {
let client = nostr_client();
let output = client.set_metadata(&new_metadata).await?;
let event = client
.database()
.event_by_id(&output.val)
.await?
.map(|event| {
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
Profile::new(event.pubkey, metadata)
});
let app_state = app_state();
let gossip = app_state.gossip.read().await;
Ok(event)
let client = nostr_client();
let signer = client.signer().await?;
// Sign the new metadata event
let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?;
// Send event to user's write relayss
gossip.send_event_to_write_relays(&event).await?;
// Return the updated profile
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
Ok(profile)
})
}

View File

@@ -1,11 +1,11 @@
use anyhow::anyhow;
use app_state::constants::{ACCOUNT_IDENTIFIER, NIP17_RELAYS, NIP65_RELAYS};
use app_state::nostr_client;
use anyhow::{anyhow, Error};
use app_state::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
use app_state::{app_state, default_nip17_relays, default_nip65_relays, nostr_client};
use common::nip96::nip96_upload;
use gpui::{
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
Render, SharedString, Styled, WeakEntity, Window,
Render, SharedString, Styled, Task, WeakEntity, Window,
};
use gpui_tokio::Tokio;
use i18n::{shared_t, t};
@@ -123,48 +123,51 @@ impl NewAccount {
self.write_keys_to_disk(&keys, password, cx);
// Set the client's signer with the current keys
cx.background_spawn(async move {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client();
let app_state = app_state();
let gossip = app_state.gossip.read().await;
// Set the client's signer with the current keys
client.set_signer(keys).await;
// Set metadata
if let Err(e) = client.set_metadata(&metadata).await {
log::error!("Failed to set metadata: {e}");
}
// Verify the signer
let signer = client.signer().await?;
// Construct a NIP-65 event
let event = EventBuilder::new(Kind::RelayList, "")
.tags(default_nip65_relays().iter().map(|(url, metadata)| {
Tag::relay_metadata(url.to_owned(), metadata.to_owned())
}))
.sign(&signer)
.await?;
// Set NIP-65 relays
let builder = EventBuilder::new(Kind::RelayList, "").tags(
NIP65_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
}),
);
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send NIP-65 relay list event: {e}");
}
// Construct a NIP-17 event
let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(
default_nip17_relays()
.iter()
.map(|url| Tag::relay(url.to_owned())),
)
.sign(&signer)
.await?;
// Set NIP-17 relays
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(
NIP17_RELAYS.into_iter().filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
}),
);
gossip.send_event_to_write_relays(&event).await?;
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {e}");
};
})
.detach();
// Construct a metadata event
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
// Set metadata
gossip.send_event_to_write_relays(&event).await?;
Ok(())
});
task.detach();
}
fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {

View File

@@ -6,7 +6,6 @@ use gpui::{
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
};
use i18n::{shared_t, t};
use nostr_sdk::prelude::*;
use registry::Registry;
use settings::AppSettings;
use theme::ActiveTheme;
@@ -54,28 +53,25 @@ impl Preferences {
.on_ok(move |_, window, cx| {
weak_view
.update(cx, |this, cx| {
let set_metadata = this.set_metadata(cx);
let registry = Registry::global(cx);
let set_metadata = this.set_metadata(cx);
cx.spawn_in(window, async move |_, cx| {
match set_metadata.await {
Ok(profile) => {
if let Some(profile) = profile {
cx.update(|_, cx| {
registry.update(cx, |this, cx| {
this.insert_or_update_person(profile, cx);
});
})
.ok();
cx.spawn_in(window, async move |this, cx| {
let result = set_metadata.await;
this.update_in(cx, |_, window, cx| {
match result {
Ok(profile) => {
registry.update(cx, |this, cx| {
this.insert_or_update_person(profile, cx);
});
}
}
Err(e) => {
cx.update(|window, cx| {
Err(e) => {
window.push_notification(e.to_string(), cx);
})
.ok();
}
};
}
};
})
.ok();
})
.detach();
})
@@ -87,7 +83,7 @@ impl Preferences {
}
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
let view = setup_relay::init(Kind::InboxRelays, window, cx);
let view = setup_relay::init(window, cx);
let weak_view = view.downgrade();
window.open_modal(cx, move |this, _window, _cx| {

View File

@@ -158,11 +158,13 @@ impl Screening {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client();
let builder = EventBuilder::report(
vec![Tag::public_key_report(public_key, Report::Impersonation)],
"scam/impersonation",
);
let _ = client.send_event_builder(builder).await?;
let signer = client.signer().await?;
let tag = Tag::public_key_report(public_key, Report::Impersonation);
let event = EventBuilder::report(vec![tag], "").sign(&signer).await?;
// Send the report to the public relays
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
Ok(())
});

View File

@@ -1,13 +1,13 @@
use std::collections::HashSet;
use std::time::Duration;
use anyhow::{anyhow, Error};
use app_state::constants::NIP17_RELAYS;
use app_state::{app_state, nostr_client};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, uniform_list, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
TextAlign, UniformList, Window,
div, px, uniform_list, App, AppContext, AsyncWindowContext, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, TextAlign, UniformList, Window,
};
use i18n::{shared_t, t};
use nostr_sdk::prelude::*;
@@ -15,100 +15,40 @@ use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput};
use ui::modal::ModalButtonProps;
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, StyledExt};
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable};
pub fn init(kind: Kind, window: &mut Window, cx: &mut App) -> Entity<SetupRelay> {
cx.new(|cx| SetupRelay::new(kind, window, cx))
}
pub fn setup_nip17_relay<T>(label: T) -> impl IntoElement
where
T: Into<SharedString>,
{
div().child(
Button::new("setup-relays")
.icon(IconName::Info)
.label(label)
.warning()
.xsmall()
.rounded()
.on_click(move |_, window, cx| {
let view = cx.new(|cx| SetupRelay::new(Kind::InboxRelays, window, cx));
let weak_view = view.downgrade();
window.open_modal(cx, move |modal, _window, _cx| {
let weak_view = weak_view.clone();
modal
.confirm()
.title(shared_t!("relays.modal"))
.child(view.clone())
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
.on_ok(move |_, window, cx| {
weak_view
.update(cx, |this, cx| {
this.set_relays(window, cx);
})
.ok();
// true to close the modal
false
})
})
}),
)
pub fn init(window: &mut Window, cx: &mut App) -> Entity<SetupRelay> {
cx.new(|cx| SetupRelay::new(window, cx))
}
#[derive(Debug)]
pub struct SetupRelay {
input: Entity<InputState>,
relays: Vec<RelayUrl>,
error: Option<SharedString>,
// All relays
relays: HashSet<RelayUrl>,
// Event subscriptions
_subscriptions: SmallVec<[Subscription; 1]>,
// Background tasks
_tasks: SmallVec<[Task<()>; 1]>,
}
impl SetupRelay {
pub fn new(kind: Kind, window: &mut Window, cx: &mut Context<Self>) -> Self {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
let load_relay = cx.background_spawn(async move {
let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new().kind(kind).author(public_key).limit(1);
if let Some(event) = client.database().query(filter).await?.first() {
let relays: Vec<RelayUrl> = event
.tags
.iter()
.filter_map(|tag| tag.as_standardized())
.filter_map(|tag| {
if let TagStandard::RelayMetadata { relay_url, .. } = tag {
Some(relay_url.to_owned())
} else if let TagStandard::Relay(url) = tag {
Some(url.to_owned())
} else {
None
}
})
.collect();
Ok(relays)
} else {
Err(anyhow!("Not found."))
}
});
tasks.push(
// Load user's relays in the local database
cx.spawn_in(window, async move |this, cx| {
if let Ok(relays) = load_relay.await {
if let Ok(relays) = Self::load(cx).await {
this.update(cx, |this, cx| {
this.relays = relays;
this.relays.extend(relays);
cx.notify();
})
.ok();
@@ -131,35 +71,55 @@ impl SetupRelay {
Self {
input,
relays: vec![],
relays: HashSet::new(),
error: None,
_subscriptions: subscriptions,
_tasks: tasks,
}
}
fn load(cx: &AsyncWindowContext) -> Task<Result<Vec<RelayUrl>, Error>> {
cx.background_spawn(async move {
let client = nostr_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let urls = nip17::extract_owned_relay_list(event).collect();
Ok(urls)
} else {
Err(anyhow!("Not found."))
}
})
}
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let value = self.input.read(cx).value().to_string();
if !value.starts_with("ws") {
self.set_error("Relay URl is invalid", window, cx);
return;
}
if let Ok(url) = RelayUrl::parse(&value) {
if !self.relays.contains(&url) {
self.relays.push(url);
if !self.relays.insert(url) {
self.input.update(cx, |this, cx| {
this.set_value("", window, cx);
});
cx.notify();
}
self.input.update(cx, |this, cx| {
this.set_value("", window, cx);
});
cx.notify();
} else {
self.set_error("Relay URl is invalid", window, cx);
}
}
fn remove(&mut self, ix: usize, _window: &mut Window, cx: &mut Context<Self>) {
self.relays.remove(ix);
fn remove(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
self.relays.remove(url);
cx.notify();
}
@@ -173,12 +133,10 @@ impl SetupRelay {
// Clear the error message after a delay
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|_, cx| {
this.update(cx, |this, cx| {
this.error = None;
cx.notify();
})
.ok();
this.update(cx, |this, cx| {
this.error = None;
cx.notify();
})
.ok();
})
@@ -198,6 +156,9 @@ impl SetupRelay {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let app_state = app_state();
let gossip = app_state.gossip.read().await;
let tags: Vec<Tag> = relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
@@ -205,21 +166,20 @@ impl SetupRelay {
let event = EventBuilder::new(Kind::InboxRelays, "")
.tags(tags)
.build(public_key)
.sign(&signer)
.await?;
// Set messaging relays
client.send_event(&event).await?;
gossip.send_event_to_write_relays(&event).await?;
// Connect to messaging relays
for relay in relays.iter() {
_ = client.add_relay(relay).await;
_ = client.connect_relay(relay).await;
client.add_relay(relay).await.ok();
client.connect_relay(relay).await.ok();
}
// Fetch gift wrap events
let sub_id = app_state().gift_wrap_sub_id.clone();
let sub_id = app_state.gift_wrap_sub_id.clone();
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
if client
@@ -259,38 +219,47 @@ impl SetupRelay {
uniform_list(
"relays",
total,
cx.processor(move |_, range, _window, cx| {
cx.processor(move |_v, range, _window, cx| {
let mut items = Vec::new();
for ix in range {
let item = relays.get(ix).map(|i: &RelayUrl| i.to_string()).unwrap();
items.push(
div().group("").w_full().h_9().py_0p5().child(
if let Some(url) = relays.iter().nth(ix) {
items.push(
div()
.px_2()
.h_full()
.id(SharedString::from(url.to_string()))
.group("")
.w_full()
.flex()
.items_center()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.text_xs()
.child(item)
.h_9()
.py_0p5()
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.xsmall()
.ghost()
.invisible()
.group_hover("", |this| this.visible())
.on_click(cx.listener(move |this, _, window, cx| {
this.remove(ix, window, cx)
})),
div()
.px_2()
.h_full()
.w_full()
.flex()
.items_center()
.justify_between()
.rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background)
.text_xs()
.child(SharedString::from(url.to_string()))
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.xsmall()
.ghost()
.invisible()
.group_hover("", |this| this.visible())
.on_click({
let url = url.to_owned();
cx.listener(move |this, _ev, _window, cx| {
this.remove(&url, cx);
})
}),
),
),
),
)
)
}
}
items
@@ -339,39 +308,6 @@ impl Render for SetupRelay {
})),
),
)
.child(
h_flex()
.gap_2()
.child(
div()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(shared_t!("common.recommended")),
)
.child(h_flex().gap_1().children({
NIP17_RELAYS.iter().map(|&relay| {
div()
.id(relay)
.group("")
.py_0p5()
.px_1p5()
.text_xs()
.text_center()
.bg(cx.theme().secondary_background)
.hover(|this| this.bg(cx.theme().secondary_hover))
.active(|this| this.bg(cx.theme().secondary_active))
.rounded_full()
.child(relay)
.on_click(cx.listener(move |this, _, window, cx| {
this.input.update(cx, |this, cx| {
this.set_value(relay, window, cx);
});
this.add(window, cx);
}))
})
})),
)
.when_some(self.error.as_ref(), |this, error| {
this.child(
div()

View File

@@ -69,7 +69,7 @@ impl Focusable for Welcome {
}
impl Render for Welcome {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()

View File

@@ -505,7 +505,7 @@ impl Room {
continue;
}
// Send the event to the relays
// Send the event to the messaging relays
match client.send_event_to(urls, &event).await {
Ok(output) => {
let id = output.id().to_owned();
@@ -556,7 +556,7 @@ impl Room {
if urls.is_empty() {
reports.push(SendReport::new(public_key).not_found());
} else {
// Send the event to the relays
// Send the event to the messaging relays
match client.send_event_to(urls, &event).await {
Ok(output) => {
reports.push(SendReport::new(public_key).status(output));
@@ -619,7 +619,7 @@ impl Room {
if urls.is_empty() {
resend_reports.push(SendReport::new(receiver).not_found());
} else {
// Send the event to the relays
// Send the event to the messaging relays
match client.send_event_to(urls, &event).await {
Ok(output) => {
resend_reports.push(SendReport::new(receiver).status(output));