wip
This commit is contained in:
@@ -12,10 +12,8 @@ nostr-lmdb.workspace = true
|
||||
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
smallvec.workspace = true
|
||||
flume.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
rustls = "0.23.23"
|
||||
rustls = "0.23"
|
||||
|
||||
46
crates/state/src/event.rs
Normal file
46
crates/state/src/event.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use nostr_sdk::prelude::*;
|
||||
use smol::lock::RwLock;
|
||||
|
||||
static TRACKER: OnceLock<Arc<RwLock<EventTracker>>> = OnceLock::new();
|
||||
|
||||
pub fn tracker() -> &'static Arc<RwLock<EventTracker>> {
|
||||
TRACKER.get_or_init(|| Arc::new(RwLock::new(EventTracker::default())))
|
||||
}
|
||||
|
||||
/// Event tracker
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EventTracker {
|
||||
/// Tracking events sent by Coop in the current session
|
||||
sent_ids: HashSet<EventId>,
|
||||
|
||||
/// Events that need to be resent later
|
||||
pending_resend: HashSet<(EventId, RelayUrl)>,
|
||||
}
|
||||
|
||||
impl EventTracker {
|
||||
/// Check if an event was sent by Coop in the current session.
|
||||
pub fn is_sent_by_coop(&self, id: &EventId) -> bool {
|
||||
self.sent_ids.contains(id)
|
||||
}
|
||||
|
||||
/// Mark an event as sent by Coop.
|
||||
pub fn sent(&mut self, id: EventId) {
|
||||
self.sent_ids.insert(id);
|
||||
}
|
||||
|
||||
/// Get all events that need to be resent later for a specific relay.
|
||||
pub fn pending_resend(&mut self, relay: &RelayUrl) -> Vec<EventId> {
|
||||
self.pending_resend
|
||||
.extract_if(|(_id, url)| url == relay)
|
||||
.map(|(id, _url)| id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Add an event (id and relay url) to the pending resend set.
|
||||
pub fn add_to_pending(&mut self, id: EventId, url: RelayUrl) {
|
||||
self.pending_resend.insert((id, url));
|
||||
}
|
||||
}
|
||||
@@ -1,80 +1,19 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gossip
|
||||
#[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> {
|
||||
/// Get read relays for a given public key
|
||||
pub fn read_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
@@ -92,8 +31,8 @@ impl Gossip {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get outbox relays for a public key
|
||||
pub fn outbox_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
/// Get write relays for a given public key
|
||||
pub fn write_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.relays
|
||||
.get(public_key)
|
||||
.map(|relays| {
|
||||
@@ -132,7 +71,7 @@ impl Gossip {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get messaging relays for a public key
|
||||
/// Get messaging relays for a given public key
|
||||
pub fn messaging_relays(&self, public_key: &PublicKey) -> Vec<RelayUrl> {
|
||||
self.messaging_relays
|
||||
.get(public_key)
|
||||
@@ -161,29 +100,4 @@ impl Gossip {
|
||||
.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);
|
||||
}
|
||||
}
|
||||
83
crates/state/src/identity.rs
Normal file
83
crates/state/src/identity.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use nostr_sdk::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum RelayState {
|
||||
#[default]
|
||||
Initial,
|
||||
NotSet,
|
||||
Set,
|
||||
}
|
||||
|
||||
impl RelayState {
|
||||
pub fn is_initial(&self) -> bool {
|
||||
matches!(self, RelayState::Initial)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Identity {
|
||||
/// The public key of the account
|
||||
public_key: Option<PublicKey>,
|
||||
|
||||
/// Status of the current user NIP-65 relays
|
||||
relay_list: RelayState,
|
||||
|
||||
/// Status of the current user NIP-17 relays
|
||||
messaging_relays: RelayState,
|
||||
}
|
||||
|
||||
impl AsRef<Identity> for Identity {
|
||||
fn as_ref(&self) -> &Identity {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
public_key: None,
|
||||
relay_list: RelayState::default(),
|
||||
messaging_relays: RelayState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the state of the NIP-65 relays.
|
||||
pub fn set_relay_list_state(&mut self, state: RelayState) {
|
||||
self.relay_list = state;
|
||||
}
|
||||
|
||||
/// Returns the state of the NIP-65 relays.
|
||||
pub fn relay_list_state(&self) -> RelayState {
|
||||
self.relay_list
|
||||
}
|
||||
|
||||
pub fn set_messaging_relays_state(&mut self, state: RelayState) {
|
||||
self.messaging_relays = state;
|
||||
}
|
||||
|
||||
/// Returns the state of the NIP-17 relays.
|
||||
pub fn messaging_relays_state(&self) -> RelayState {
|
||||
self.messaging_relays
|
||||
}
|
||||
|
||||
/// Returns true if the identity has a public key.
|
||||
pub fn has_public_key(&self) -> bool {
|
||||
self.public_key.is_some()
|
||||
}
|
||||
|
||||
/// Sets the public key of the identity.
|
||||
pub fn set_public_key(&mut self, public_key: PublicKey) {
|
||||
self.public_key = Some(public_key);
|
||||
}
|
||||
|
||||
/// Unsets the public key of the identity.
|
||||
pub fn unset_public_key(&mut self) {
|
||||
self.public_key = None;
|
||||
}
|
||||
|
||||
/// Returns the public key of the identity.
|
||||
pub fn public_key(&self) -> PublicKey {
|
||||
// This method is safe to unwrap because the public key is always called when the identity is created.
|
||||
self.public_key.unwrap()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,32 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||
use anyhow::Error;
|
||||
use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Task};
|
||||
use gpui::{App, AppContext, Context, Entity, Global, Subscription, 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;
|
||||
mod event;
|
||||
mod gossip;
|
||||
mod identity;
|
||||
|
||||
pub const GIFTWRAP_SUBSCRIPTION: &str = "gift-wrap-events";
|
||||
pub use event::*;
|
||||
pub use gossip::*;
|
||||
pub use identity::*;
|
||||
|
||||
use crate::identity::Identity;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
|
||||
}
|
||||
|
||||
/// Default timeout for subscription
|
||||
pub const TIMEOUT: u64 = 3;
|
||||
|
||||
/// Default subscription id for gift wrap events
|
||||
pub const GIFTWRAP_SUBSCRIPTION: &str = "giftwrap-events";
|
||||
|
||||
struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
||||
|
||||
impl Global for GlobalNostrRegistry {}
|
||||
@@ -28,17 +34,27 @@ impl Global for GlobalNostrRegistry {}
|
||||
/// Nostr Registry
|
||||
#[derive(Debug)]
|
||||
pub struct NostrRegistry {
|
||||
/// Nostr Client
|
||||
/// Nostr client
|
||||
client: Client,
|
||||
|
||||
/// Custom gossip implementation
|
||||
gossip: Arc<RwLock<Gossip>>,
|
||||
/// App keys
|
||||
///
|
||||
/// Used for Nostr Connect and NIP-4e operations
|
||||
app_keys: Keys,
|
||||
|
||||
/// Tracks activity related to Nostr events
|
||||
tracker: Arc<RwLock<EventTracker>>,
|
||||
/// Current identity (user's public key)
|
||||
///
|
||||
/// Set by the current Nostr signer
|
||||
identity: Entity<Identity>,
|
||||
|
||||
/// Gossip implementation
|
||||
gossip: Entity<Gossip>,
|
||||
|
||||
/// Tasks for asynchronous operations
|
||||
_tasks: SmallVec<[Task<()>; 1]>,
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
|
||||
/// Subscriptions
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl NostrRegistry {
|
||||
@@ -79,310 +95,424 @@ impl NostrRegistry {
|
||||
|
||||
// Construct the nostr client
|
||||
let client = ClientBuilder::default().database(lmdb).opts(opts).build();
|
||||
let _ = tracker();
|
||||
|
||||
let tracker = Arc::new(RwLock::new(EventTracker::default()));
|
||||
let gossip = Arc::new(RwLock::new(Gossip::default()));
|
||||
// Get the app keys
|
||||
let app_keys = Self::create_or_init_app_keys().unwrap();
|
||||
|
||||
let mut tasks = smallvec![];
|
||||
// Construct the gossip entity
|
||||
let gossip = cx.new(|_| Gossip::default());
|
||||
let async_gossip = gossip.downgrade();
|
||||
|
||||
// Construct the identity entity
|
||||
let identity = cx.new(|_| Identity::default());
|
||||
|
||||
// Channel for communication between nostr and gpui
|
||||
let (tx, rx) = flume::bounded::<Event>(2048);
|
||||
|
||||
let mut subscriptions = vec![];
|
||||
let mut tasks = vec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the identity entity
|
||||
cx.observe(&identity, |this, state, cx| {
|
||||
let identity = state.read(cx);
|
||||
|
||||
if identity.has_public_key() {
|
||||
match identity.relay_list_state() {
|
||||
RelayState::Initial => {
|
||||
this.get_relay_list(cx);
|
||||
}
|
||||
RelayState::Set => match identity.messaging_relays_state() {
|
||||
RelayState::Initial => {
|
||||
this.get_messaging_relays(cx);
|
||||
}
|
||||
RelayState::Set => {
|
||||
this.get_messages(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
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;
|
||||
// Add bootstrap relay to the relay pool
|
||||
for url in BOOTSTRAP_RELAYS.into_iter() {
|
||||
client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Handle notifications from the relay pool
|
||||
Self::handle_notifications(&client, &gossip, &tracker).await;
|
||||
// Add search relay to the relay pool
|
||||
for url in SEARCH_RELAYS.into_iter() {
|
||||
client.add_relay(url).await?;
|
||||
}
|
||||
|
||||
// Connect to all added relays
|
||||
client.connect().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Handle nostr notifications
|
||||
cx.background_spawn({
|
||||
let client = client.clone();
|
||||
|
||||
async move { Self::handle_notifications(&client, &tx).await }
|
||||
}),
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Update GPUI states
|
||||
cx.spawn(async move |_this, cx| {
|
||||
while let Ok(event) = rx.recv_async().await {
|
||||
match event.kind {
|
||||
Kind::RelayList => {
|
||||
async_gossip.update(cx, |this, cx| {
|
||||
this.insert_relays(&event);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
async_gossip.update(cx, |this, cx| {
|
||||
this.insert_messaging_relays(&event);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
client,
|
||||
tracker,
|
||||
identity,
|
||||
gossip,
|
||||
_tasks: tasks,
|
||||
app_keys,
|
||||
_subscriptions: subscriptions,
|
||||
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>>,
|
||||
) {
|
||||
// Handle nostr notifications
|
||||
async fn handle_notifications(client: &Client, tx: &flume::Sender<Event>) -> Result<(), Error> {
|
||||
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;
|
||||
};
|
||||
if let RelayPoolNotification::Message { message, relay_url } = notification {
|
||||
match message {
|
||||
RelayMessage::Event { event, .. } => {
|
||||
if !processed_events.insert(event.id) {
|
||||
// Skip if the event has already been processed
|
||||
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 => {
|
||||
tx.send_async(event.into_owned()).await?;
|
||||
}
|
||||
Kind::InboxRelays => {
|
||||
tx.send_async(event.into_owned()).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
RelayMessage::Ok {
|
||||
event_id, message, ..
|
||||
} => {
|
||||
let msg = MachineReadablePrefix::parse(&message);
|
||||
let mut tracker = tracker().write().await;
|
||||
|
||||
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;
|
||||
}
|
||||
// Handle authentication messages
|
||||
if let Some(MachineReadablePrefix::AuthRequired) = msg {
|
||||
// Keep track of events that need to be resent after authentication
|
||||
tracker.add_to_pending(event_id, relay_url);
|
||||
} else {
|
||||
// Keep track of events sent by Coop
|
||||
tracker.sent(event_id)
|
||||
}
|
||||
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()
|
||||
/// Get or create a new app keys
|
||||
fn create_or_init_app_keys() -> Result<Keys, Error> {
|
||||
let dir = config_dir().join(".app_keys");
|
||||
let content = match std::fs::read(&dir) {
|
||||
Ok(content) => content,
|
||||
Err(_) => {
|
||||
// Generate new keys if file doesn't exist
|
||||
let keys = Keys::generate();
|
||||
let secret_key = keys.secret_key();
|
||||
|
||||
std::fs::create_dir_all(dir.parent().unwrap())?;
|
||||
std::fs::write(&dir, secret_key.to_secret_bytes())?;
|
||||
|
||||
return Ok(keys);
|
||||
}
|
||||
};
|
||||
let secret_key = SecretKey::from_slice(&content)?;
|
||||
let keys = Keys::new(secret_key);
|
||||
|
||||
Ok(keys)
|
||||
}
|
||||
|
||||
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.
|
||||
/// Get 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)
|
||||
/// Get the app keys
|
||||
pub fn app_keys(&self) -> &Keys {
|
||||
&self.app_keys
|
||||
}
|
||||
|
||||
/// Returns a reference to the cache manager.
|
||||
pub fn gossip(&self) -> Arc<RwLock<Gossip>> {
|
||||
Arc::clone(&self.gossip)
|
||||
/// Get current identity
|
||||
pub fn identity(&self, cx: &App) -> Identity {
|
||||
self.identity.read(cx).clone()
|
||||
}
|
||||
|
||||
/// Get a relay hint (messaging relay) for a given public key
|
||||
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option<RelayUrl> {
|
||||
self.gossip
|
||||
.read(cx)
|
||||
.messaging_relays(public_key)
|
||||
.first()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Get a list of messaging relays for a given public key
|
||||
pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<RelayUrl> {
|
||||
self.gossip.read(cx).messaging_relays(public_key)
|
||||
}
|
||||
|
||||
/// Set the signer for the nostr client and verify the public key
|
||||
pub fn set_signer<T>(&mut self, signer: T, cx: &mut Context<Self>)
|
||||
where
|
||||
T: NostrSigner + 'static,
|
||||
{
|
||||
let client = self.client();
|
||||
let identity = self.identity.downgrade();
|
||||
|
||||
// Create a task to update the signer and verify the public key
|
||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||
// Update signer
|
||||
client.set_signer(signer).await;
|
||||
|
||||
// Verify signer
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
Ok(public_key)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
match task.await {
|
||||
Ok(public_key) => {
|
||||
identity.update(cx, |this, cx| {
|
||||
this.set_public_key(public_key);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to set signer: {e}");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Unset the current signer
|
||||
pub fn unset_signer(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
let async_identity = self.identity.downgrade();
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
// Unset the signer from nostr client
|
||||
cx.background_executor()
|
||||
.await_on_background(async move {
|
||||
client.unset_signer().await;
|
||||
})
|
||||
.await;
|
||||
|
||||
// Unset the current identity
|
||||
async_identity
|
||||
.update(cx, |this, cx| {
|
||||
this.unset_public_key();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
// Get relay list for current user
|
||||
fn get_relay_list(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
let async_identity = self.identity.downgrade();
|
||||
let public_key = self.identity(cx).public_key();
|
||||
|
||||
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::RelayList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
let mut stream = client
|
||||
.stream_events_from(BOOTSTRAP_RELAYS, vec![filter], Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
if let Ok(event) = res {
|
||||
log::info!("Received relay list event: {event:?}");
|
||||
return Ok(RelayState::Set);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RelayState::NotSet)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
match task.await {
|
||||
Ok(state) => {
|
||||
async_identity
|
||||
.update(cx, |this, cx| {
|
||||
this.set_relay_list_state(state);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get relay list: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get messaging relays for current user
|
||||
fn get_messaging_relays(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
let async_identity = self.identity.downgrade();
|
||||
let public_key = self.identity(cx).public_key();
|
||||
let write_relays = self.gossip.read(cx).write_relays(&public_key);
|
||||
|
||||
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::InboxRelays)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
let mut stream = client
|
||||
.stream_events_from(write_relays, vec![filter], Duration::from_secs(TIMEOUT))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
if let Ok(event) = res {
|
||||
log::info!("Received messaging relays event: {event:?}");
|
||||
return Ok(RelayState::Set);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(RelayState::NotSet)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |_this, cx| {
|
||||
match task.await {
|
||||
Ok(state) => {
|
||||
async_identity
|
||||
.update(cx, |this, cx| {
|
||||
this.set_messaging_relays_state(state);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to get messaging relays: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Continuously get gift wrap events for the current user in their messaging relays
|
||||
fn get_messages(&mut self, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
let public_key = self.identity(cx).public_key();
|
||||
let messaging_relays = self.gossip.read(cx).messaging_relays(&public_key);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let id = SubscriptionId::new(GIFTWRAP_SUBSCRIPTION);
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id_to(messaging_relays, id, vec![filter], None)
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to subscribe to gift wrap events: {e}");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Publish an event to author's write relays
|
||||
pub fn publish(&self, event: Event, cx: &App) -> Task<Result<Output<EventId>, Error>> {
|
||||
let client = self.client();
|
||||
let write_relays = self.gossip.read(cx).write_relays(&event.pubkey);
|
||||
|
||||
cx.background_spawn(async move { Ok(client.send_event_to(&write_relays, &event).await?) })
|
||||
}
|
||||
|
||||
/// Subscribe to event kinds to author's write relays
|
||||
pub fn subscribe<I>(&self, kinds: I, author: PublicKey, cx: &App)
|
||||
where
|
||||
I: Into<Vec<Kind>>,
|
||||
{
|
||||
let client = self.client();
|
||||
let write_relays = self.gossip.read(cx).write_relays(&author);
|
||||
|
||||
// Construct filters based on event kinds
|
||||
let filters: Vec<Filter> = kinds
|
||||
.into()
|
||||
.into_iter()
|
||||
.map(|kind| Filter::new().kind(kind).author(author).limit(1))
|
||||
.collect();
|
||||
|
||||
// Construct subscription options
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.timeout(Some(Duration::from_secs(TIMEOUT)))
|
||||
.exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = client
|
||||
.subscribe_to(&write_relays, filters, Some(opts))
|
||||
.await
|
||||
{
|
||||
log::error!("Failed to create a subscription: {e}");
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user