fix(nostr): profile publish broadcasts to ALL enabled relays

Previously handle_identity_publish_profile defaulted to a single
hard-coded relay (ws://localhost:18081) so the user's kind:0 profile
event only ever landed on the local relay — hence "Manage Relays
shows N connected, but profile edits don't propagate" from testing.

Fix — two-layer change:

- identity_manager::publish_profile now takes `&[String]` relays
  instead of one URL. Adds each relay to the nostr-sdk client,
  gives 15s for handshakes, publishes, then surfaces per-relay
  accept/reject in a new ProfilePublishOutcome struct so the UI
  can show WHICH relays accepted vs. rejected and WHY.
- RPC handle_identity_publish_profile no longer defaults to the
  local relay: pulls the ENABLED list from nostr_relays::list_relays
  (the same table that powers Manage Relays) and publishes to every
  entry. Accepts an optional `relays: [...]` override for tests.
- At-least-one-accept guarantee: if every relay rejects, the call
  errors instead of silently reporting published=true. User gets a
  real error message listing the failures.
- Response shape: `{event_id, accepted: [urls], rejected: [[url,
  reason]], relays_attempted: N, published: bool}` so the UI can
  show a useful status block after clicking Publish.

relay_url_matches is tolerant of trailing-slash / case differences
since nostr-sdk canonicalises URLs internally.

Covers the publishing half of task #29; avatar/banner upload UI is
still open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-19 04:42:25 -04:00
co-authored by Claude Opus 4.7
parent f756365935
commit b77b031b8e
2 changed files with 131 additions and 22 deletions
@@ -727,7 +727,10 @@ impl RpcHandler {
Ok(serde_json::json!({ "ok": true }))
}
/// Publish kind 0 (metadata) profile to the local Nostr relay.
/// Publish kind 0 (metadata) profile to every enabled Nostr relay
/// configured in Manage Relays. Callers can override the default
/// list by passing `relays: [..]` in params (e.g. to publish to a
/// single relay for testing).
pub(in crate::api::rpc) async fn handle_identity_publish_profile(
&self,
params: Option<serde_json::Value>,
@@ -739,18 +742,38 @@ impl RpcHandler {
.ok_or_else(|| anyhow::anyhow!("Missing required parameter: id"))?;
validate_identity_id(id)?;
let relay_url = params
.get("relay")
.and_then(|v| v.as_str())
.unwrap_or("ws://localhost:18081");
let relay_urls: Vec<String> = if let Some(arr) = params.get("relays").and_then(|v| v.as_array()) {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect()
} else if let Some(single) = params.get("relay").and_then(|v| v.as_str()) {
vec![single.to_string()]
} else {
// Default: every enabled relay in the user's Manage Relays list.
let statuses = crate::nostr_relays::list_relays(&self.config.data_dir)
.await
.unwrap_or_default();
statuses
.into_iter()
.filter(|s| s.enabled)
.map(|s| s.url)
.collect()
};
if relay_urls.is_empty() {
anyhow::bail!("No enabled relays configured; add one under Manage Relays");
}
let manager = IdentityManager::new(&self.config.data_dir).await?;
let event_id = manager.publish_profile(id, relay_url).await?;
let outcome = manager.publish_profile(id, &relay_urls).await?;
Ok(serde_json::json!({
"event_id": event_id,
"relay": relay_url,
"published": true,
"event_id": outcome.event_id,
"accepted": outcome.accepted,
"rejected": outcome.rejected,
"relays_attempted": relay_urls.len(),
"published": !outcome.accepted.is_empty(),
}))
}