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 tokio::sync::Mutex;
|
||||
|
||||
use nostr_sdk::prelude::PublicKey;
|
||||
|
||||
use crate::app::{App, Nip46ClientSignerHandle};
|
||||
use crate::errors::AppError;
|
||||
use crate::feed;
|
||||
|
|
@ -331,6 +333,107 @@ pub async fn serve() -> Result<(), AppError> {
|
|||
|
||||
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 {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -553,6 +553,19 @@ pub fn find_stored_profile<'a>(
|
|||
.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.
|
||||
///
|
||||
/// 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)]
|
||||
mod tests {
|
||||
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};
|
||||
|
||||
fn populated_vault() -> Vault {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue