diff --git a/core/archipelago/src/api/rpc/federation/handlers.rs b/core/archipelago/src/api/rpc/federation/handlers.rs index d80690f0..5a63e9bf 100644 --- a/core/archipelago/src/api/rpc/federation/handlers.rs +++ b/core/archipelago/src/api/rpc/federation/handlers.rs @@ -262,9 +262,36 @@ impl RpcHandler { .ok_or_else(|| anyhow::anyhow!("Missing 'did' parameter"))?; validate_did(did)?; + // Capture the node's pubkey before removal so we can also purge its + // synthetic mesh contact/thread (#2) — remove_node only touches + // nodes.json, which would otherwise leave a stale chat contact behind. + let removed_pubkey = federation::load_nodes(&self.config.data_dir) + .await + .ok() + .and_then(|nodes| { + nodes + .into_iter() + .find(|n| n.did == did) + .map(|n| n.pubkey) + }); + let nodes = federation::remove_node(&self.config.data_dir, did).await?; info!(did = %did, "Removed node from federation"); + if let Some(pubkey) = removed_pubkey.filter(|p| !p.is_empty()) { + let svc = self.mesh_service.read().await; + if let Some(svc) = svc.as_ref() { + let contact_id = mesh::federation_peer_contact_id(&pubkey); + mesh::purge_federation_peer( + &svc.shared_state(), + contact_id, + &pubkey, + &self.config.data_dir, + ) + .await; + } + } + Ok(serde_json::json!({ "removed": true, "nodes_remaining": nodes.len(), diff --git a/core/archipelago/src/mesh/mod.rs b/core/archipelago/src/mesh/mod.rs index 618348d1..46984dd3 100644 --- a/core/archipelago/src/mesh/mod.rs +++ b/core/archipelago/src/mesh/mod.rs @@ -88,6 +88,38 @@ pub(crate) async fn upsert_federation_peer( contact_id } +/// Purge a federation peer from all live mesh state and persisted contacts so +/// removing a node (federation.remove-node) also clears its chat contact, +/// thread, and any per-contact customisation — otherwise a stale/renamed node +/// (e.g. an old "Arch HP" entry) lingers in the chat list even after it's gone +/// from `nodes.json` (#2). Keyed by the synthetic `contact_id` for the peer +/// table/messages and by `pubkey_hex` for the pubkey-keyed contacts/presence +/// stores. +pub(crate) async fn purge_federation_peer( + state: &Arc, + contact_id: u32, + pubkey_hex: &str, + data_dir: &Path, +) { + state.peers.write().await.remove(&contact_id); + state.shared_secrets.write().await.remove(&contact_id); + state + .messages + .write() + .await + .retain(|m| m.peer_contact_id != contact_id); + state.presence.write().await.remove(pubkey_hex); + let mut contacts = state.contacts.write().await; + if contacts.remove(pubkey_hex).is_some() { + let snapshot = contacts.clone(); + drop(contacts); + if let Err(e) = save_mesh_contacts(data_dir, &snapshot).await { + warn!("Failed to persist mesh contacts after purge: {}", e); + } + } + state.update_peer_count().await; +} + /// Load federation nodes from disk and upsert each as a synthetic mesh peer. /// Called at MeshService startup so the chat list already contains every /// known federation node — users can share files to them without first