feat(profiles): persistent identity backfill for generic pairing labels
Pairing-time kind-0 enrichment gives up after ~4 attempts/2 minutes; a fresh Amber account that has not published metadata yet (or publishes later) then keeps the generic 'Amber'/'Remote Signer' label forever. serve() now spawns a slow-cadence backfill: every pass it re-scans for Nip46Client rows still wearing a GENERIC_PAIRING_LABELS placeholder and re-fetches kind-0, upgrading label/picture/nip05 when metadata appears. User renames are never overwritten (placeholder check gates the write); locked encrypted vaults are skipped; network runs off-lock with a 90s budget per identity.
This commit is contained in:
parent
a6182e3cb2
commit
5b60ae3736
2 changed files with 130 additions and 0 deletions
103
src/ipc.rs
103
src/ipc.rs
|
|
@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use nostr_sdk::prelude::PublicKey;
|
||||||
|
|
||||||
use crate::app::{App, Nip46ClientSignerHandle};
|
use crate::app::{App, Nip46ClientSignerHandle};
|
||||||
use crate::errors::AppError;
|
use crate::errors::AppError;
|
||||||
use crate::feed;
|
use crate::feed;
|
||||||
|
|
@ -331,6 +333,107 @@ pub async fn serve() -> Result<(), AppError> {
|
||||||
|
|
||||||
let mut tasks = JoinSet::new();
|
let mut tasks = JoinSet::new();
|
||||||
|
|
||||||
|
// Persistent identity backfill. Pairing-time enrichment tries for ~2
|
||||||
|
// minutes and stops; if the account's kind-0 was never published (or was
|
||||||
|
// published later), the row would keep its generic "Amber"/"Remote
|
||||||
|
// Signer" placeholder forever — which is what a new user with a fresh
|
||||||
|
// Amber account sees, with no way to know the app resolved nothing.
|
||||||
|
// This loop retries on a slow cadence while the backend runs: every pass
|
||||||
|
// collects remote rows still wearing a generic placeholder (only once
|
||||||
|
// the vault is unlocked — an encrypted vault hides them otherwise),
|
||||||
|
// fetches kind-0 from the enabled relays, and upgrades the row. A user
|
||||||
|
// rename always wins: is_generic_pairing_label is the sole gate on the
|
||||||
|
// label write. Cheap when nothing is pending (one vault scan per pass).
|
||||||
|
{
|
||||||
|
let app = app.clone();
|
||||||
|
tasks.spawn(async move {
|
||||||
|
loop {
|
||||||
|
// Collect pending identities without holding the lock
|
||||||
|
// across network work.
|
||||||
|
let pending: Vec<(String, PublicKey)> = {
|
||||||
|
let guard = app.lock().await;
|
||||||
|
if guard.is_locked() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
guard
|
||||||
|
.vault
|
||||||
|
.profiles
|
||||||
|
.iter()
|
||||||
|
.filter(|p| {
|
||||||
|
p.signer_mode == SignerMode::Nip46Client
|
||||||
|
&& profiles::is_generic_pairing_label(&p.label)
|
||||||
|
})
|
||||||
|
.filter_map(|p| {
|
||||||
|
PublicKey::parse(&p.public_key)
|
||||||
|
.ok()
|
||||||
|
.map(|key| (p.public_key.clone(), key))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if pending.is_empty() {
|
||||||
|
tokio::time::sleep(Duration::from_secs(300)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let relay_urls = {
|
||||||
|
let guard = app.lock().await;
|
||||||
|
relays::enabled_urls(&guard.settings)
|
||||||
|
};
|
||||||
|
for (npub, identity) in pending {
|
||||||
|
let found = tokio::time::timeout(
|
||||||
|
Duration::from_secs(90),
|
||||||
|
tokio::task::spawn_blocking({
|
||||||
|
let relay_urls = relay_urls.clone();
|
||||||
|
move || profiles::fetch_profile_metadata(&identity, &relay_urls)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|join| join.ok())
|
||||||
|
.flatten();
|
||||||
|
let Some(meta) = found else { continue };
|
||||||
|
let mut guard = app.lock().await;
|
||||||
|
let mut changed = false;
|
||||||
|
// Re-resolve by pubkey string: another request (rename,
|
||||||
|
// a pairing-time enrichment) may have edited the row
|
||||||
|
// while this fetch was in flight.
|
||||||
|
if let Some(row) = guard
|
||||||
|
.vault
|
||||||
|
.profiles
|
||||||
|
.iter_mut()
|
||||||
|
.find(|p| p.public_key == npub)
|
||||||
|
{
|
||||||
|
if row.picture.is_none() && meta.picture.is_some() {
|
||||||
|
row.picture = meta.picture.clone();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if row.nip05.is_none() && meta.nip05.is_some() {
|
||||||
|
row.nip05 = meta.nip05.clone();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
let real_name = meta
|
||||||
|
.display_name
|
||||||
|
.as_deref()
|
||||||
|
.or(meta.name.as_deref())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|name| !name.is_empty());
|
||||||
|
if let Some(name) = real_name {
|
||||||
|
if profiles::is_generic_pairing_label(&row.label) {
|
||||||
|
row.label = name.to_string();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
let _ = guard.save_vault();
|
||||||
|
eprintln!("[backfill] resolved profile identity via kind-0: {npub}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_secs(120)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(line) = line_rx.recv().await {
|
while let Some(line) = line_rx.recv().await {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -553,6 +553,19 @@ pub fn find_stored_profile<'a>(
|
||||||
.ok_or_else(|| AppError::profile_not_found(npub))
|
.ok_or_else(|| AppError::profile_not_found(npub))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Labels the UI assigns when pairing without a user-supplied name
|
||||||
|
/// (CreateProfileModal defaults to "Amber"; Signer Mode to "Remote
|
||||||
|
/// Signer"). A remote-signer profile row still carrying one of these has
|
||||||
|
/// never resolved its real identity from the network, so it stays a
|
||||||
|
/// candidate for automatic name backfill on every launch. A user rename
|
||||||
|
/// always falls outside this list and is therefore never overwritten.
|
||||||
|
pub const GENERIC_PAIRING_LABELS: &[&str] = &["Amber", "Remote Signer"];
|
||||||
|
|
||||||
|
/// True when `label` is one of the generic pairing placeholders.
|
||||||
|
pub fn is_generic_pairing_label(label: &str) -> bool {
|
||||||
|
GENERIC_PAIRING_LABELS.contains(&label)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create or refresh the vault profile for a remote (NIP-46) identity.
|
/// Create or refresh the vault profile for a remote (NIP-46) identity.
|
||||||
///
|
///
|
||||||
/// When a NIP-46 client connection is established the identity lives on the
|
/// When a NIP-46 client connection is established the identity lives on the
|
||||||
|
|
@ -930,6 +943,20 @@ pub fn delete_profile(vault: &mut Vault, npub: &str) -> Result<ProfileSummary, A
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generic_pairing_labels_gate_the_backfill() {
|
||||||
|
// The background backfill upgrades a row's label ONLY while it still
|
||||||
|
// wears one of the placeholders the UI assigns at pairing time.
|
||||||
|
assert!(is_generic_pairing_label("Amber"));
|
||||||
|
assert!(is_generic_pairing_label("Remote Signer"));
|
||||||
|
// Any real name — fetched or user-typed — is never a candidate, so
|
||||||
|
// automatic enrichment can never overwrite it.
|
||||||
|
assert!(!is_generic_pairing_label("satoshi"));
|
||||||
|
assert!(!is_generic_pairing_label("My Profile"));
|
||||||
|
assert!(!is_generic_pairing_label("amber"));
|
||||||
|
assert!(!is_generic_pairing_label(""));
|
||||||
|
}
|
||||||
use crate::vault::{KdfParams, Vault, VaultCrypto};
|
use crate::vault::{KdfParams, Vault, VaultCrypto};
|
||||||
|
|
||||||
fn populated_vault() -> Vault {
|
fn populated_vault() -> Vault {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue