This commit is contained in:
2026-01-05 10:51:25 +07:00
parent 23f8cc49c6
commit e1ed8483ba
10 changed files with 96 additions and 722 deletions

17
Cargo.lock generated
View File

@@ -6024,23 +6024,6 @@ dependencies = [
"smol", "smol",
] ]
[[package]]
name = "state_old"
version = "0.3.0"
dependencies = [
"anyhow",
"common",
"gpui",
"log",
"nostr-lmdb",
"nostr-sdk",
"rustls",
"serde",
"serde_json",
"smallvec",
"smol",
]
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"

View File

@@ -11,9 +11,7 @@ use flume::Sender;
use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task}; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Task};
pub use message::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
pub use room::*;
use settings::AppSettings; use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::{tracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION}; use state::{tracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
@@ -21,6 +19,9 @@ use state::{tracker, NostrRegistry, GIFTWRAP_SUBSCRIPTION};
mod message; mod message;
mod room; mod room;
pub use message::*;
pub use room::*;
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
ChatRegistry::set_global(cx.new(ChatRegistry::new), cx); ChatRegistry::set_global(cx.new(ChatRegistry::new), cx);
} }
@@ -42,17 +43,22 @@ pub struct ChatRegistry {
_tasks: SmallVec<[Task<()>; 4]>, _tasks: SmallVec<[Task<()>; 4]>,
} }
/// Chat event.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent { pub enum ChatEvent {
OpenRoom(u64), OpenRoom(u64),
CloseRoom(u64), CloseRoom(u64),
NewChatRequest(RoomKind), NewRequest(RoomKind),
} }
/// Channel signal.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Signal { enum Signal {
Loading(bool), /// Message received from relay pool
Message(NewMessage), Message(NewMessage),
/// Loading status of the registry
Loading(bool),
/// Eose received from relay pool
Eose, Eose,
} }
@@ -507,7 +513,6 @@ impl ChatRegistry {
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
let is_new_event = message.rumor.created_at > room.read(cx).created_at; let is_new_event = message.rumor.created_at > room.read(cx).created_at;
let created_at = message.rumor.created_at; let created_at = message.rumor.created_at;
let event_for_emit = message.rumor.clone();
// Update room // Update room
room.update(cx, |this, cx| { room.update(cx, |this, cx| {
@@ -521,7 +526,7 @@ impl ChatRegistry {
} }
// Emit the new message to the room // Emit the new message to the room
this.emit_message(message.gift_wrap, event_for_emit.clone(), cx); this.emit_message(message, cx);
}); });
// Resort all rooms in the registry by their created at (after updated) // Resort all rooms in the registry by their created at (after updated)
@@ -533,7 +538,7 @@ impl ChatRegistry {
self.add_room(cx.new(|_| Room::from(&message.rumor)), cx); self.add_room(cx.new(|_| Room::from(&message.rumor)), cx);
// Notify the UI about the new room // Notify the UI about the new room
cx.emit(ChatEvent::NewChatRequest(RoomKind::default())); cx.emit(ChatEvent::NewRequest(RoomKind::default()));
} }
} }

View File

@@ -2,6 +2,7 @@ use std::hash::Hash;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
/// New message.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NewMessage { pub struct NewMessage {
pub gift_wrap: EventId, pub gift_wrap: EventId,
@@ -14,6 +15,7 @@ impl NewMessage {
} }
} }
/// Message.
#[derive(Debug, Clone, Hash, PartialEq, Eq)] #[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum Message { pub enum Message {
User(RenderedMessage), User(RenderedMessage),
@@ -22,11 +24,17 @@ pub enum Message {
} }
impl Message { impl Message {
pub fn user(user: impl Into<RenderedMessage>) -> Self { pub fn user<I>(user: I) -> Self
where
I: Into<RenderedMessage>,
{
Self::User(user.into()) Self::User(user.into())
} }
pub fn warning(content: impl Into<String>) -> Self { pub fn warning<I>(content: I) -> Self
where
I: Into<String>,
{
Self::Warning(content.into(), Timestamp::now()) Self::Warning(content.into(), Timestamp::now())
} }
@@ -43,6 +51,18 @@ impl Message {
} }
} }
impl From<&NewMessage> for Message {
fn from(val: &NewMessage) -> Self {
Self::User(val.into())
}
}
impl From<&UnsignedEvent> for Message {
fn from(val: &UnsignedEvent) -> Self {
Self::User(val.into())
}
}
impl Ord for Message { impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering { fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) { match (self, other) {
@@ -63,6 +83,7 @@ impl PartialOrd for Message {
} }
} }
/// Rendered message.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RenderedMessage { pub struct RenderedMessage {
pub id: EventId, pub id: EventId,
@@ -78,48 +99,53 @@ pub struct RenderedMessage {
pub replies_to: Vec<EventId>, pub replies_to: Vec<EventId>,
} }
impl From<Event> for RenderedMessage { impl From<&Event> for RenderedMessage {
fn from(inner: Event) -> Self { fn from(val: &Event) -> Self {
let mentions = extract_mentions(&inner.content); let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&inner.tags); let replies_to = extract_reply_ids(&val.tags);
Self { Self {
id: inner.id, id: val.id,
author: inner.pubkey, author: val.pubkey,
content: inner.content, content: val.content.clone(),
created_at: inner.created_at, created_at: val.created_at,
mentions, mentions,
replies_to, replies_to,
} }
} }
} }
impl From<UnsignedEvent> for RenderedMessage { impl From<&UnsignedEvent> for RenderedMessage {
fn from(inner: UnsignedEvent) -> Self { fn from(val: &UnsignedEvent) -> Self {
let mentions = extract_mentions(&inner.content); let mentions = extract_mentions(&val.content);
let replies_to = extract_reply_ids(&inner.tags); let replies_to = extract_reply_ids(&val.tags);
Self { Self {
// Event ID must be known // Event ID must be known
id: inner.id.unwrap(), id: val.id.unwrap(),
author: inner.pubkey, author: val.pubkey,
content: inner.content, content: val.content.clone(),
created_at: inner.created_at, created_at: val.created_at,
mentions, mentions,
replies_to, replies_to,
} }
} }
} }
impl From<Box<Event>> for RenderedMessage { impl From<&NewMessage> for RenderedMessage {
fn from(inner: Box<Event>) -> Self { fn from(val: &NewMessage) -> Self {
(*inner).into() let mentions = extract_mentions(&val.rumor.content);
} let replies_to = extract_reply_ids(&val.rumor.tags);
}
impl From<&Box<Event>> for RenderedMessage { Self {
fn from(inner: &Box<Event>) -> Self { // Event ID must be known
inner.to_owned().into() id: val.rumor.id.unwrap(),
author: val.rumor.pubkey,
content: val.rumor.content.clone(),
created_at: val.rumor.created_at,
mentions,
replies_to,
}
} }
} }
@@ -149,6 +175,7 @@ impl Hash for RenderedMessage {
} }
} }
/// Extracts all mentions (public keys) from a content string.
fn extract_mentions(content: &str) -> Vec<PublicKey> { fn extract_mentions(content: &str) -> Vec<PublicKey> {
let parser = NostrParser::new(); let parser = NostrParser::new();
let tokens = parser.parse(content); let tokens = parser.parse(content);
@@ -165,6 +192,7 @@ fn extract_mentions(content: &str) -> Vec<PublicKey> {
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
/// Extracts all reply (ids) from the event tags.
fn extract_reply_ids(inner: &Tags) -> Vec<EventId> { fn extract_reply_ids(inner: &Tags) -> Vec<EventId> {
let mut replies_to = vec![]; let mut replies_to = vec![];

View File

@@ -11,6 +11,8 @@ use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use state::{tracker, NostrRegistry}; use state::{tracker, NostrRegistry};
use crate::NewMessage;
const SEND_RETRY: usize = 10; const SEND_RETRY: usize = 10;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -80,17 +82,21 @@ impl SendReport {
} }
} }
#[derive(Debug, Clone)] /// Room event.
pub enum RoomSignal { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
NewMessage((EventId, UnsignedEvent)), pub enum RoomEvent {
Refresh, /// Incoming message.
Incoming(NewMessage),
/// Reloads the current room's messages.
Reload,
} }
/// Room kind.
#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum RoomKind { pub enum RoomKind {
Ongoing,
#[default] #[default]
Request, Request,
Ongoing,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -133,7 +139,7 @@ impl Hash for Room {
impl Eq for Room {} impl Eq for Room {}
impl EventEmitter<RoomSignal> for Room {} impl EventEmitter<RoomEvent> for Room {}
impl From<&UnsignedEvent> for Room { impl From<&UnsignedEvent> for Room {
fn from(val: &UnsignedEvent) -> Self { fn from(val: &UnsignedEvent) -> Self {
@@ -304,13 +310,13 @@ impl Room {
} }
/// Emits a new message signal to the current room /// Emits a new message signal to the current room
pub fn emit_message(&self, id: EventId, event: UnsignedEvent, cx: &mut Context<Self>) { pub fn emit_message(&self, message: NewMessage, cx: &mut Context<Self>) {
cx.emit(RoomSignal::NewMessage((id, event))); cx.emit(RoomEvent::Incoming(message));
} }
/// Emits a signal to refresh the current room's messages. /// Emits a signal to reload the current room's messages.
pub fn emit_refresh(&mut self, cx: &mut Context<Self>) { pub fn emit_refresh(&mut self, cx: &mut Context<Self>) {
cx.emit(RoomSignal::Refresh); cx.emit(RoomEvent::Reload);
} }
/// Get gossip relays for each member /// Get gossip relays for each member

View File

@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::time::Duration; use std::time::Duration;
pub use actions::*; pub use actions::*;
use chat::{Message, RenderedMessage, Room, RoomKind, RoomSignal, SendReport}; use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport};
use common::{nip96_upload, RenderedProfile, RenderedTimestamp}; use common::{nip96_upload, RenderedProfile, RenderedTimestamp};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -114,7 +114,7 @@ impl ChatPanel {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match result { match result {
Ok(events) => { Ok(events) => {
this.insert_messages(events, cx); this.insert_messages(&events, cx);
} }
Err(e) => { Err(e) => {
window.push_notification(e.to_string(), cx); window.push_notification(e.to_string(), cx);
@@ -142,18 +142,10 @@ impl ChatPanel {
// Subscribe to room events // Subscribe to room events
cx.subscribe_in(&room, window, move |this, _, signal, window, cx| { cx.subscribe_in(&room, window, move |this, _, signal, window, cx| {
match signal { match signal {
RoomSignal::NewMessage((gift_wrap_id, event)) => { RoomEvent::Incoming(message) => {
let message = Message::user(event.clone()); this.insert_message(message, false, cx);
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, _window, cx| {
this.insert_message(message, false, cx);
})
.ok();
})
.detach();
} }
RoomSignal::Refresh => { RoomEvent::Reload => {
this.load_messages(window, cx); this.load_messages(window, cx);
} }
}; };
@@ -202,7 +194,7 @@ impl ChatPanel {
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match result { match result {
Ok(events) => { Ok(events) => {
this.insert_messages(events, cx); this.insert_messages(&events, cx);
} }
Err(e) => { Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx); window.push_notification(Notification::error(e.to_string()), cx);
@@ -278,14 +270,16 @@ impl ChatPanel {
// Update the message list and reset the states // Update the message list and reset the states
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
this.insert_message(Message::user(rumor), true, cx);
this.remove_all_replies(cx); this.remove_all_replies(cx);
this.remove_all_attachments(cx); this.remove_all_attachments(cx);
// Reset the input to its default state
this.input.update(cx, |this, cx| { this.input.update(cx, |this, cx| {
this.set_loading(false, cx); this.set_loading(false, cx);
this.set_disabled(false, cx); this.set_disabled(false, cx);
this.set_value("", window, cx); this.set_value("", window, cx);
}); });
// Update the message list
this.insert_message(&rumor, true, cx);
}) })
.ok(); .ok();
}) })
@@ -378,11 +372,10 @@ impl ChatPanel {
} }
/// Convert and insert a vector of nostr events into the chat panel /// Convert and insert a vector of nostr events into the chat panel
fn insert_messages(&mut self, events: Vec<UnsignedEvent>, cx: &mut Context<Self>) { fn insert_messages(&mut self, events: &[UnsignedEvent], cx: &mut Context<Self>) {
for event in events { for event in events.iter() {
let m = Message::user(event);
// Bulk inserting messages, so no need to scroll to the latest message // Bulk inserting messages, so no need to scroll to the latest message
self.insert_message(m, false, cx); self.insert_message(event, false, cx);
} }
} }

View File

@@ -84,7 +84,7 @@ impl Sidebar {
subscriptions.push( subscriptions.push(
// Subscribe for registry new events // Subscribe for registry new events
cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| { cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| {
if let ChatEvent::NewChatRequest(kind) = event { if let ChatEvent::NewRequest(kind) = event {
this.indicator.update(cx, |this, cx| { this.indicator.update(cx, |this, cx| {
*this = Some(kind.to_owned()); *this = Some(kind.to_owned());
cx.notify(); cx.notify();

View File

@@ -1,21 +0,0 @@
[package]
name = "state_old"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
nostr-sdk.workspace = true
nostr-lmdb.workspace = true
gpui.workspace = true
smol.workspace = true
smallvec.workspace = true
log.workspace = true
anyhow.workspace = true
serde.workspace = true
serde_json.workspace = true
rustls = "0.23.23"

View File

@@ -1,388 +0,0 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use gpui::{App, AppContext, Context, Entity, Global, Task};
use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use smol::lock::RwLock;
pub use storage::*;
pub use tracker::*;
mod storage;
mod tracker;
pub const GIFTWRAP_SUBSCRIPTION: &str = "gift-wrap-events";
pub fn init(cx: &mut App) {
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
}
struct GlobalNostrRegistry(Entity<NostrRegistry>);
impl Global for GlobalNostrRegistry {}
/// Nostr Registry
#[derive(Debug)]
pub struct NostrRegistry {
/// Nostr Client
client: Client,
/// Custom gossip implementation
gossip: Arc<RwLock<Gossip>>,
/// Tracks activity related to Nostr events
tracker: Arc<RwLock<EventTracker>>,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
}
impl NostrRegistry {
/// Retrieve the global nostr state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalNostrRegistry>().0.clone()
}
/// Set the global nostr instance
fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalNostrRegistry(state));
}
/// Create a new nostr instance
fn new(cx: &mut Context<Self>) -> Self {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
// Construct the nostr client options
let opts = ClientOptions::new()
.automatic_authentication(false)
.verify_subscriptions(false)
.sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(600),
});
// Construct the lmdb
let lmdb = cx.background_executor().block(async move {
let path = config_dir().join("nostr");
NostrLmdb::open(path)
.await
.expect("Failed to initialize database")
});
// Construct the nostr client
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
let tracker = Arc::new(RwLock::new(EventTracker::default()));
let gossip = Arc::new(RwLock::new(Gossip::default()));
let mut tasks = smallvec![];
tasks.push(
// Establish connection to the bootstrap relays
//
// And handle notifications from the nostr relay pool channel
cx.background_spawn({
let client = client.clone();
let gossip = Arc::clone(&gossip);
let tracker = Arc::clone(&tracker);
let _ = initialized_at();
async move {
// Connect to the bootstrap relays
Self::connect(&client).await;
// Handle notifications from the relay pool
Self::handle_notifications(&client, &gossip, &tracker).await;
}
}),
);
Self {
client,
tracker,
gossip,
_tasks: tasks,
}
}
/// Establish connection to the bootstrap relays
async fn connect(client: &Client) {
// Get all bootstrapping relays
let mut urls = vec![];
urls.extend(BOOTSTRAP_RELAYS);
urls.extend(SEARCH_RELAYS);
// Add relay to the relay pool
for url in urls.into_iter() {
client.add_relay(url).await.ok();
}
// Connect to all added relays
client.connect().await;
}
async fn handle_notifications(
client: &Client,
gossip: &Arc<RwLock<Gossip>>,
tracker: &Arc<RwLock<EventTracker>>,
) {
let mut notifications = client.notifications();
let mut processed_events = HashSet::new();
while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, relay_url } = notification else {
// Skip if the notification is not a message
continue;
};
match message {
RelayMessage::Event { event, .. } => {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
match event.kind {
Kind::RelayList => {
let mut gossip = gossip.write().await;
gossip.insert_relays(&event);
let urls: Vec<RelayUrl> = Self::extract_write_relays(&event);
let author = event.pubkey;
log::info!("Write relays: {urls:?}");
// 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::get(client, &urls, author, Kind::Metadata).await;
// Fetch user's contact list event
Self::get(client, &urls, author, Kind::ContactList).await;
}
}
Kind::InboxRelays => {
let mut gossip = gossip.write().await;
gossip.insert_messaging_relays(&event);
if Self::is_self_authored(client, &event).await {
// Extract user's messaging relays
let urls: Vec<RelayUrl> =
nip17::extract_relay_list(&event).cloned().collect();
// Fetch user's inbox messages in the extracted relays
Self::get_messages(client, event.pubkey, &urls).await;
}
}
Kind::Custom(10044) => {
let mut gossip = gossip.write().await;
gossip.insert_announcement(&event);
}
Kind::ContactList => {
if Self::is_self_authored(client, &event).await {
let public_keys: Vec<PublicKey> =
event.tags.public_keys().copied().collect();
if let Err(e) =
Self::get_metadata_for_list(client, public_keys).await
{
log::error!("Failed to get metadata for list: {e}");
}
}
}
_ => {}
};
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker.write().await;
// Message that need to be authenticated will be handled separately
if let Some(MachineReadablePrefix::AuthRequired) = msg {
// Keep track of events that need to be resent after authentication
tracker.resend_queue.insert(event_id, relay_url);
} else {
// Keep track of events sent by Coop
tracker.sent_ids.insert(event_id);
}
}
_ => {}
}
}
}
/// Check if event is published by current user
pub async fn is_self_authored(client: &Client, event: &Event) -> bool {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
return public_key == event.pubkey;
}
}
false
}
/// 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
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
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);
// Unsubscribe from the previous subscription
client.unsubscribe(&id).await;
// Subscribe to filters to user's messaging relays
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];
// Return if the list is empty
if pubkeys.is_empty() {
return Err(anyhow!("You need at least one public key".to_string(),));
}
let filter = Filter::new()
.limit(pubkeys.len() * kinds.len())
.authors(pubkeys)
.kinds(kinds);
// Subscribe to filters to the bootstrap relays
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await?;
Ok(())
}
pub fn extract_read_relays(event: &Event) -> Vec<RelayUrl> {
nip65::extract_relay_list(event)
.filter_map(|(url, metadata)| {
if metadata.is_none() || metadata == &Some(RelayMetadata::Read) {
Some(url.to_owned())
} else {
None
}
})
.take(3)
.collect()
}
pub 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
.tags
.iter()
.find(|tag| tag.kind().as_str() == "n" || tag.kind().as_str() == "pubkey")
.and_then(|tag| tag.content())
.and_then(|c| PublicKey::parse(c).ok())
.context("Cannot parse public key from the event's tags")?;
let client_name = event
.tags
.find(TagKind::Client)
.and_then(|tag| tag.content())
.map(|c| c.to_string());
Ok(Announcement::new(event.id, client_name, public_key))
}
/// Extract an encryption keys response from an event.
pub async fn extract_response(client: &Client, event: &Event) -> Result<Response, Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
if event.pubkey != public_key {
return Err(anyhow!("Event does not belong to current user"));
}
let client_pubkey = event
.tags
.find(TagKind::custom("P"))
.and_then(|tag| tag.content())
.and_then(|c| PublicKey::parse(c).ok())
.context("Cannot parse public key from the event's tags")?;
Ok(Response::new(event.content.clone(), client_pubkey))
}
/// Returns a reference to the nostr client.
pub fn client(&self) -> Client {
self.client.clone()
}
/// Returns a reference to the event tracker.
pub fn tracker(&self) -> Arc<RwLock<EventTracker>> {
Arc::clone(&self.tracker)
}
/// Returns a reference to the cache manager.
pub fn gossip(&self) -> Arc<RwLock<Gossip>> {
Arc::clone(&self.gossip)
}
}

View File

@@ -1,189 +0,0 @@
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,
public_key: PublicKey,
client_name: Option<String>,
}
impl Announcement {
pub fn new(id: EventId, client_name: Option<String>, public_key: PublicKey) -> Self {
Self {
id,
client_name,
public_key,
}
}
pub fn id(&self) -> EventId {
self.id
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn client_name(&self) -> SharedString {
self.client_name
.as_ref()
.map(SharedString::from)
.unwrap_or(SharedString::from("Unknown"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Response {
payload: String,
public_key: PublicKey,
}
impl Response {
pub fn new(payload: String, public_key: PublicKey) -> Self {
Self {
payload,
public_key,
}
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn payload(&self) -> &str {
self.payload.as_str()
}
}
#[derive(Debug, Clone, Default)]
pub struct Gossip {
/// Gossip relays for each public key
relays: HashMap<PublicKey, HashSet<(RelayUrl, Option<RelayMetadata>)>>,
/// Messaging relays for each public key
messaging_relays: HashMap<PublicKey, HashSet<RelayUrl>>,
/// Encryption announcement for each public key
announcements: HashMap<PublicKey, Option<Announcement>>,
}
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::Read) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
/// 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::Write) {
Some(url.to_owned())
} else {
None
}
})
.collect()
})
.unwrap_or_default()
}
/// 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),
);
}
/// 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

@@ -1,43 +0,0 @@
use std::collections::{HashMap, HashSet};
use std::sync::OnceLock;
use nostr_sdk::prelude::*;
static INITIALIZED_AT: OnceLock<Timestamp> = OnceLock::new();
pub fn initialized_at() -> &'static Timestamp {
INITIALIZED_AT.get_or_init(Timestamp::now)
}
#[derive(Debug, Clone, Default)]
pub struct EventTracker {
/// Tracking events that have been resent by Coop in the current session
pub resent_ids: Vec<Output<EventId>>,
/// Temporarily store events that need to be resent later
pub resend_queue: HashMap<EventId, RelayUrl>,
/// Tracking events sent by Coop in the current session
pub sent_ids: HashSet<EventId>,
/// Tracking events seen on which relays in the current session
pub seen_on_relays: HashMap<EventId, HashSet<RelayUrl>>,
}
impl EventTracker {
pub fn resent_ids(&self) -> &Vec<Output<EventId>> {
&self.resent_ids
}
pub fn resend_queue(&self) -> &HashMap<EventId, RelayUrl> {
&self.resend_queue
}
pub fn sent_ids(&self) -> &HashSet<EventId> {
&self.sent_ids
}
pub fn seen_on_relays(&self) -> &HashMap<EventId, HashSet<RelayUrl>> {
&self.seen_on_relays
}
}