update
This commit is contained in:
+133
-143
@@ -243,160 +243,135 @@ impl Backend {
|
||||
/// Decrypt the NIP-49 encrypted credential stored in the keyring with
|
||||
/// the given passphrase and resume the session.
|
||||
///
|
||||
/// The scrypt decryption runs off the UI thread. The returned receiver
|
||||
/// The scrypt decryption runs off the UI thread. The returned task
|
||||
/// yields the public key on success, or the failure reason (e.g. wrong
|
||||
/// passphrase), so callers can render inline errors.
|
||||
pub fn restore_with_passphrase(
|
||||
&mut self,
|
||||
password: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> flume::Receiver<Result<PublicKey, Error>> {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
|
||||
) -> Task<Result<PublicKey, Error>> {
|
||||
let password = password.to_owned();
|
||||
let user = cx.read_credentials(USER_KEYRING);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = async {
|
||||
let content = user
|
||||
.await?
|
||||
.map(|(_username, secret)| String::from_utf8(secret))
|
||||
.transpose()?
|
||||
.ok_or_else(|| anyhow!("no stored credential; nothing to unlock"))?;
|
||||
cx.spawn(async move |this, cx| {
|
||||
let content = user
|
||||
.await?
|
||||
.map(|(_username, secret)| String::from_utf8(secret))
|
||||
.transpose()?
|
||||
.ok_or_else(|| anyhow!("no stored credential; nothing to unlock"))?;
|
||||
|
||||
if !content.starts_with("ncryptsec1") {
|
||||
Err(anyhow!("stored credential is not passphrase-encrypted"))?;
|
||||
}
|
||||
|
||||
let decrypt_task = cx.background_spawn(async move {
|
||||
let encrypted = EncryptedSecretKey::from_bech32(&content)?;
|
||||
let secret = encrypted.decrypt(&password)?;
|
||||
Ok::<_, Error>(Keys::new(secret))
|
||||
});
|
||||
|
||||
let keys = decrypt_task.await?;
|
||||
let public_key = keys.public_key();
|
||||
|
||||
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
|
||||
|
||||
Ok::<_, Error>(public_key)
|
||||
if !content.starts_with("ncryptsec1") {
|
||||
return Err(anyhow!("stored credential is not passphrase-encrypted"));
|
||||
}
|
||||
.await;
|
||||
|
||||
tx.send_async(result).await.ok();
|
||||
Ok(())
|
||||
}));
|
||||
let decrypt_task = cx.background_spawn(async move {
|
||||
let encrypted = EncryptedSecretKey::from_bech32(&content)?;
|
||||
let secret = encrypted.decrypt(&password)?;
|
||||
Ok::<_, Error>(Keys::new(secret))
|
||||
});
|
||||
|
||||
rx
|
||||
let keys = decrypt_task.await?;
|
||||
let public_key = keys.public_key();
|
||||
|
||||
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
|
||||
|
||||
Ok(public_key)
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new identity: generate keys, encrypt the secret key with the
|
||||
/// passphrase (NIP-49) and persist it in the keyring, then publish the
|
||||
/// user's NIP-65 relay list, metadata and grasp list.
|
||||
///
|
||||
/// The heavy encryption runs off the UI thread. The returned receiver
|
||||
/// yields the new public key on success, or the failure reason, so
|
||||
/// callers can render progress and inline errors.
|
||||
/// The heavy encryption runs off the UI thread. The returned task yields
|
||||
/// the new public key on success, or the failure reason, so callers can
|
||||
/// render progress and inline errors.
|
||||
pub fn create_identity(
|
||||
&mut self,
|
||||
name: &str,
|
||||
password: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> flume::Receiver<Result<PublicKey, Error>> {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
|
||||
) -> Task<Result<PublicKey, Error>> {
|
||||
let name = name.trim().to_owned();
|
||||
let password = password.to_owned();
|
||||
|
||||
let validation_error = if name.is_empty() || name.len() > 255 {
|
||||
Some("Name must be 1-255 characters")
|
||||
} else if password.is_empty() {
|
||||
Some("Passphrase must not be empty")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(message) = validation_error {
|
||||
tx.try_send(Err(anyhow!(message))).ok();
|
||||
return rx;
|
||||
if name.is_empty() || name.len() > 255 {
|
||||
return Task::ready(Err(anyhow!("Name must be 1-255 characters")));
|
||||
}
|
||||
if password.is_empty() {
|
||||
return Task::ready(Err(anyhow!("Passphrase must not be empty")));
|
||||
}
|
||||
|
||||
let job = cx.background_spawn(async move {
|
||||
let keys = Keys::generate();
|
||||
let encrypted =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 16, KeySecurity::Medium)?;
|
||||
let ncryptsec = encrypted.to_bech32()?;
|
||||
Ok::<_, Error>((keys, ncryptsec))
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
let job = cx.background_spawn(async move {
|
||||
let keys = Keys::generate();
|
||||
let encrypted =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 16, KeySecurity::Medium)?;
|
||||
let ncryptsec = encrypted.to_bech32()?;
|
||||
Ok::<_, Error>((keys, ncryptsec))
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = async {
|
||||
let (keys, ncryptsec) = job.await?;
|
||||
let public_key = keys.public_key();
|
||||
let (keys, ncryptsec) = job.await?;
|
||||
let public_key = keys.public_key();
|
||||
|
||||
// Persist the encrypted credential.
|
||||
let write = cx.update(|cx| {
|
||||
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), ncryptsec.as_bytes())
|
||||
});
|
||||
write.await?;
|
||||
// Persist the encrypted credential.
|
||||
let write = cx.update(|cx| {
|
||||
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), ncryptsec.as_bytes())
|
||||
});
|
||||
write.await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
// Become the new identity, so the publishes below are
|
||||
// signed with the new keys.
|
||||
this.signer.swap_inner(keys);
|
||||
this.current_user = Some(public_key);
|
||||
this.bootstrap_user(public_key, cx);
|
||||
cx.emit(BackendEvent::SignerChanged);
|
||||
cx.notify();
|
||||
this.update(cx, |this, cx| {
|
||||
// Become the new identity, so the publishes below are
|
||||
// signed with the new keys.
|
||||
this.signer.swap_inner(keys);
|
||||
this.current_user = Some(public_key);
|
||||
this.bootstrap_user(public_key, cx);
|
||||
cx.emit(BackendEvent::SignerChanged);
|
||||
cx.notify();
|
||||
|
||||
let relays: Vec<(RelayUrl, Option<RelayMetadata>)> = [
|
||||
(
|
||||
RelayUrl::parse("wss://relay.primal.net").unwrap(),
|
||||
Some(RelayMetadata::Read),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://relay.ditto.pub").unwrap(),
|
||||
Some(RelayMetadata::Read),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://relay.nostr.net").unwrap(),
|
||||
Some(RelayMetadata::Write),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://nos.lol").unwrap(),
|
||||
Some(RelayMetadata::Write),
|
||||
),
|
||||
]
|
||||
.to_vec();
|
||||
let relays: Vec<(RelayUrl, Option<RelayMetadata>)> = [
|
||||
(
|
||||
RelayUrl::parse("wss://relay.primal.net").unwrap(),
|
||||
Some(RelayMetadata::Read),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://relay.ditto.pub").unwrap(),
|
||||
Some(RelayMetadata::Read),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://relay.nostr.net").unwrap(),
|
||||
Some(RelayMetadata::Write),
|
||||
),
|
||||
(
|
||||
RelayUrl::parse("wss://nos.lol").unwrap(),
|
||||
Some(RelayMetadata::Write),
|
||||
),
|
||||
]
|
||||
.to_vec();
|
||||
|
||||
this.send(RelayList::new(relays).into_event_builder(), cx);
|
||||
this.send_fire_and_forget(RelayList::new(relays).into_event_builder(), cx);
|
||||
|
||||
let metadata = Metadata::new()
|
||||
.name(&name)
|
||||
.display_name(&name)
|
||||
.into_event_builder();
|
||||
let metadata = Metadata::new()
|
||||
.name(&name)
|
||||
.display_name(&name)
|
||||
.into_event_builder();
|
||||
|
||||
this.send(metadata, cx);
|
||||
this.send_fire_and_forget(metadata, cx);
|
||||
|
||||
let grasp_servers: Vec<RelayUrl> =
|
||||
["wss://gitnostr.com", "wss://relay.ngit.dev"]
|
||||
.into_iter()
|
||||
.map(|url| RelayUrl::parse(url).expect("valid relay URL"))
|
||||
.collect();
|
||||
let grasp_servers: Vec<RelayUrl> = ["wss://gitnostr.com", "wss://relay.ngit.dev"]
|
||||
.into_iter()
|
||||
.map(|url| RelayUrl::parse(url).expect("valid relay URL"))
|
||||
.collect();
|
||||
|
||||
this.send(GitUserGraspList { grasp_servers }.into_event_builder(), cx);
|
||||
})?;
|
||||
this.send_fire_and_forget(
|
||||
GitUserGraspList { grasp_servers }.into_event_builder(),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
Ok(public_key)
|
||||
}
|
||||
.await;
|
||||
|
||||
tx.send_async(result).await.ok();
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
rx
|
||||
Ok(public_key)
|
||||
})
|
||||
}
|
||||
|
||||
/// Login with an `nsec1...` key or a `bunker://...` URI, dispatching on
|
||||
@@ -786,59 +761,74 @@ impl Backend {
|
||||
/// Sign, broadcast and locally store an event. Emits
|
||||
/// [`BackendEvent::Published`] on success so stores can refresh.
|
||||
///
|
||||
/// The returned receiver yields the outcome of this specific action,
|
||||
/// so callers can show inline progress/errors instead of relying on
|
||||
/// the global [`BackendEvent::Error`].
|
||||
/// The returned task yields the outcome of this specific action, so
|
||||
/// callers can show inline progress/errors instead of relying on
|
||||
/// the global [`BackendEvent::Error`]. The task is owned by the caller;
|
||||
/// dropping it cancels the publish.
|
||||
pub fn send(
|
||||
&mut self,
|
||||
builder: EventBuilder,
|
||||
cx: &mut Context<Self>,
|
||||
) -> flume::Receiver<Result<Event, Error>> {
|
||||
let (tx, rx) = flume::bounded(1);
|
||||
) -> Task<Result<Event, Error>> {
|
||||
let client = self.client.clone();
|
||||
let signer = self.signer.clone();
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
cx.spawn(async move |this, cx| {
|
||||
// Sign with the current signer, broadcast, and save locally so
|
||||
// the event is immediately visible to database queries.
|
||||
let event = builder.finalize_async(&signer).await?;
|
||||
let output = client.send_event(&event).await?;
|
||||
let work = cx.background_spawn(async move {
|
||||
let event = builder.finalize_async(&signer).await?;
|
||||
let output = client.send_event(&event).await?;
|
||||
|
||||
if output.success.is_empty() && !output.failed.is_empty() {
|
||||
let reasons = output
|
||||
.failed
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
return Err(anyhow!("event not accepted by any relay: {reasons}"));
|
||||
}
|
||||
if output.success.is_empty() && !output.failed.is_empty() {
|
||||
let reasons = output
|
||||
.failed
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
return Err(anyhow!("event not accepted by any relay: {reasons}"));
|
||||
}
|
||||
|
||||
Ok(event)
|
||||
});
|
||||
Ok(event)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
let result = work.await;
|
||||
|
||||
match &result {
|
||||
Ok(event) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(BackendEvent::Published(Box::new(event.clone())));
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(BackendEvent::error(e.to_string()));
|
||||
})?;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
tx.send_async(result)
|
||||
.await
|
||||
.map_err(|_| anyhow!("action result receiver dropped"))
|
||||
}));
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
rx
|
||||
/// Sign, broadcast and store an event without awaiting the result;
|
||||
/// failures surface through [`BackendEvent::Error`]. The spawned task is
|
||||
/// owned by the backend, so it is cancelled when the backend is dropped.
|
||||
fn send_fire_and_forget(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
||||
let task = self.send(builder, cx);
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |_this, cx| {
|
||||
cx.emit(BackendEvent::error(e.to_string()));
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::RwLock;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Error;
|
||||
@@ -73,8 +73,8 @@ const BATCH_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
/// data; the whole store notifies on change.
|
||||
pub struct ProfileStore {
|
||||
profiles: HashMap<PublicKey, Profile>,
|
||||
/// Public keys we've already requested this session.
|
||||
seen: RwLock<HashSet<PublicKey>>,
|
||||
/// Public keys we've already requested this session (main thread only).
|
||||
seen: RefCell<HashSet<PublicKey>>,
|
||||
/// Sender for queuing fetch requests, batched by a background task.
|
||||
sender: Sender<PublicKey>,
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
@@ -133,7 +133,7 @@ impl ProfileStore {
|
||||
|
||||
let mut store = Self {
|
||||
profiles: HashMap::new(),
|
||||
seen: RwLock::new(HashSet::new()),
|
||||
seen: RefCell::new(HashSet::new()),
|
||||
sender,
|
||||
tasks,
|
||||
_subscription: subscription,
|
||||
@@ -152,7 +152,7 @@ impl ProfileStore {
|
||||
|
||||
let public_key = *public_key;
|
||||
|
||||
if self.seen.write().unwrap().insert(public_key)
|
||||
if self.seen.borrow_mut().insert(public_key)
|
||||
&& let Err(e) = self.sender.send(public_key)
|
||||
{
|
||||
log::warn!("failed to queue profile fetch: {e}");
|
||||
@@ -232,7 +232,8 @@ impl ProfileStore {
|
||||
/// Re-read the latest metadata of every requested author from the local
|
||||
/// database (used after a sync, which produces no NostrUpdate events).
|
||||
fn apply_seen(&mut self, cx: &mut Context<Self>) {
|
||||
let authors: Vec<PublicKey> = self.seen.read().unwrap().iter().copied().collect();
|
||||
let authors: Vec<PublicKey> = self.seen.borrow().iter().copied().collect();
|
||||
|
||||
if authors.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,9 +94,10 @@ impl RepoStore {
|
||||
/// Fetch this repository's events from the bootstrap relays (one-shot,
|
||||
/// auto-closing subscription).
|
||||
fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
|
||||
let backend = Backend::global(cx);
|
||||
let addr = self.addr.clone();
|
||||
|
||||
Backend::global(cx).update(cx, |backend, cx| {
|
||||
backend.update(cx, |backend, cx| {
|
||||
let mut repo_filters = vec![
|
||||
filters::announcement(&addr),
|
||||
filters::state(&addr),
|
||||
@@ -309,20 +310,18 @@ impl RepoStore {
|
||||
fn send(&mut self, builder: EventBuilder, cx: &mut Context<Self>) {
|
||||
self.last_error = None;
|
||||
|
||||
let rx = Backend::global(cx).update(cx, |backend, cx| backend.send(builder, cx));
|
||||
let backend = Backend::global(cx);
|
||||
let task = backend.update(cx, |backend, cx| backend.send(builder, cx));
|
||||
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
if let Ok(Err(e)) = rx.recv_async().await {
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
if let Err(e) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_error = Some(e.to_string());
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
self.tasks.push(task);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user