.
This commit is contained in:
@@ -44,18 +44,12 @@ impl Global for GlobalNostrRegistry {}
|
||||
/// Signer event.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum StateEvent {
|
||||
/// Creating the signer
|
||||
Creating,
|
||||
/// Connecting to the bootstrapping relay
|
||||
Connecting,
|
||||
/// Connected to the bootstrapping relay
|
||||
Connected,
|
||||
/// Fetching the relay list
|
||||
FetchingRelayList,
|
||||
/// User has not set up NIP-65 relays
|
||||
RelayNotConfigured,
|
||||
/// Connected to NIP-65 relays
|
||||
RelayConnected,
|
||||
/// Creating the signer
|
||||
Creating,
|
||||
/// A new signer has been set
|
||||
SignerSet,
|
||||
/// An error occurred
|
||||
@@ -154,6 +148,10 @@ impl NostrRegistry {
|
||||
// Run at the end of current cycle
|
||||
cx.defer_in(window, |this, _window, cx| {
|
||||
this.connect(cx);
|
||||
// Create an identity if none exists
|
||||
if this.npubs.read(cx).is_empty() {
|
||||
this.create_identity(cx);
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -465,7 +463,6 @@ impl NostrRegistry {
|
||||
{
|
||||
let client = self.client();
|
||||
let signer = self.signer();
|
||||
let key_dir = self.key_dir.clone();
|
||||
|
||||
// Create a task to update the signer and verify the public key
|
||||
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
|
||||
@@ -578,118 +575,6 @@ impl NostrRegistry {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Ensure the relay list is fetched for the given public key
|
||||
pub fn ensure_relay_list(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||
let task = self.get_event(public_key, Kind::RelayList, cx);
|
||||
|
||||
// Emit a fetching event before starting the task
|
||||
cx.emit(StateEvent::FetchingRelayList);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(event) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.ensure_connection(&event, cx);
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayNotConfigured);
|
||||
cx.emit(StateEvent::error(e.to_string()));
|
||||
})?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Ensure that the user is connected to the relay specified in the NIP-65 event.
|
||||
pub fn ensure_connection(&mut self, event: &Event, cx: &mut Context<Self>) {
|
||||
let client = self.client();
|
||||
// Extract the relay list from the event
|
||||
let relays: Vec<(RelayUrl, Option<RelayMetadata>)> = nip65::extract_relay_list(event)
|
||||
.map(|(url, metadata)| (url.to_owned(), metadata.to_owned()))
|
||||
.collect();
|
||||
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
for (url, metadata) in relays.into_iter() {
|
||||
match metadata {
|
||||
Some(RelayMetadata::Read) => {
|
||||
client
|
||||
.add_relay(url)
|
||||
.capabilities(RelayCapabilities::READ)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.and_connect()
|
||||
.await?;
|
||||
}
|
||||
Some(RelayMetadata::Write) => {
|
||||
client
|
||||
.add_relay(url)
|
||||
.capabilities(RelayCapabilities::WRITE)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.and_connect()
|
||||
.await?;
|
||||
}
|
||||
None => {
|
||||
client
|
||||
.add_relay(url)
|
||||
.capabilities(RelayCapabilities::NONE)
|
||||
.connect_timeout(Duration::from_secs(TIMEOUT))
|
||||
.and_connect()
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(_) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayConnected);
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(StateEvent::RelayNotConfigured);
|
||||
cx.emit(StateEvent::error(e.to_string()));
|
||||
})?;
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Get an event with the given author and kind.
|
||||
pub fn get_event(
|
||||
&self,
|
||||
author: &PublicKey,
|
||||
kind: Kind,
|
||||
cx: &App,
|
||||
) -> Task<Result<Event, Error>> {
|
||||
let client = self.client();
|
||||
let public_key = *author;
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let filter = Filter::new().kind(kind).author(public_key).limit(1);
|
||||
let mut stream = client
|
||||
.stream_events(filter)
|
||||
.timeout(Duration::from_millis(800))
|
||||
.await?;
|
||||
|
||||
while let Some((_url, res)) = stream.next().await {
|
||||
if let Ok(event) = res {
|
||||
return Ok(event);
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No event found"))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the public key of a NIP-05 address
|
||||
pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task<Result<PublicKey, Error>> {
|
||||
let client = self.client();
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Error, anyhow};
|
||||
use common::config_dir;
|
||||
use gpui::{App, Context};
|
||||
use nostr_connect::prelude::*;
|
||||
|
||||
use crate::{CLIENT_NAME, NOSTR_CONNECT_TIMEOUT};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NostrRing {
|
||||
/// Keys directory
|
||||
dir: PathBuf,
|
||||
|
||||
/// Master app keys used for various operations.
|
||||
///
|
||||
/// Example: Nostr Connect and NIP-4e operations
|
||||
app_keys: Keys,
|
||||
|
||||
/// All local stored identities
|
||||
npubs: Vec<PublicKey>,
|
||||
}
|
||||
|
||||
impl NostrRing {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let dir = config_dir().join("keys");
|
||||
let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate());
|
||||
|
||||
// Get all local stored npubs
|
||||
let npubs = match Self::discover(&dir) {
|
||||
Ok(npubs) => npubs,
|
||||
Err(e) => {
|
||||
log::error!("Failed to discover npubs: {e}");
|
||||
vec![]
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
dir,
|
||||
app_keys,
|
||||
npubs,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the secret for a given npub, if it exists
|
||||
fn get_secret(&self, public_key: &PublicKey, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
|
||||
let npub = public_key.to_bech32()?;
|
||||
let key_path = self.dir.join(format!("{}.npub", npub));
|
||||
|
||||
if let Ok(secret) = std::fs::read_to_string(key_path) {
|
||||
let secret = SecretKey::parse(&secret)?;
|
||||
let keys = Keys::new(secret);
|
||||
|
||||
Ok(keys.into_nostr_signer())
|
||||
} else {
|
||||
self.get_secret_keyring(&npub, cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the secret for a given npub in the os credentials store
|
||||
#[deprecated = "Use get_secret instead"]
|
||||
fn get_secret_keyring(&self, user: &str, cx: &App) -> Result<Arc<dyn NostrSigner>, Error> {
|
||||
let read = cx.read_credentials(user);
|
||||
let app_keys = self.app_keys.clone();
|
||||
|
||||
cx.foreground_executor().block_on(async move {
|
||||
let (_, secret) = read
|
||||
.await
|
||||
.map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))?
|
||||
.ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?;
|
||||
|
||||
// Try to parse as a direct secret key first
|
||||
if let Ok(secret_key) = SecretKey::from_slice(&secret) {
|
||||
return Ok(Keys::new(secret_key).into_nostr_signer());
|
||||
}
|
||||
|
||||
// Convert the secret into string
|
||||
let sec = String::from_utf8(secret)
|
||||
.map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?;
|
||||
|
||||
// Try to parse as a NIP-46 URI
|
||||
let uri =
|
||||
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
||||
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
||||
|
||||
// Set the auth URL handler
|
||||
nip46.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
Ok(nip46.into_nostr_signer())
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a new npub to the keys directory
|
||||
fn add(&mut self, public_key: PublicKey, secret: &str) -> Result<(), Error> {
|
||||
let npub = public_key.to_bech32()?;
|
||||
let key_path = self.dir.join(format!("{}.npub", npub));
|
||||
std::fs::write(key_path, secret)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a npub from the keys directory
|
||||
fn remove(&self, public_key: &PublicKey) -> Result<(), Error> {
|
||||
let npub = public_key.to_bech32()?;
|
||||
let key_path = self.dir.join(format!("{}.npub", npub));
|
||||
std::fs::remove_file(key_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover all npubs in the keys directory
|
||||
fn discover(dir: &PathBuf) -> Result<Vec<PublicKey>, Error> {
|
||||
// Ensure keys directory exists
|
||||
std::fs::create_dir_all(dir)?;
|
||||
|
||||
let files = std::fs::read_dir(dir)?;
|
||||
let mut entries = Vec::new();
|
||||
let mut npubs: Vec<PublicKey> = Vec::new();
|
||||
|
||||
for file in files.flatten() {
|
||||
let metadata = file.metadata()?;
|
||||
let modified_time = metadata.modified()?;
|
||||
let name = file.file_name().into_string().unwrap().replace(".npub", "");
|
||||
entries.push((modified_time, name));
|
||||
}
|
||||
|
||||
// Sort by modification time (most recent first)
|
||||
entries.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
for (_, name) in entries {
|
||||
let public_key = PublicKey::parse(&name)?;
|
||||
npubs.push(public_key);
|
||||
}
|
||||
|
||||
Ok(npubs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user