fix(federation): remove-node also purges the mesh contact/thread (#2)

federation.remove-node only edited nodes.json, so a removed/renamed node
(e.g. a stale "Arch HP") lingered in the mesh chat list with its old
thread. Capture the node's pubkey before removal, then purge its
synthetic mesh peer, shared secret, messages, presence, and persisted
contact entry via the new mesh::purge_federation_peer. Combined with the
#42 name refresh, stale federation contacts can now be fully cleaned from
a node.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-06-17 06:12:56 -04:00
parent d2d2b9dd68
commit ab56054aeb
2 changed files with 59 additions and 0 deletions

View File

@ -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(),

View File

@ -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<listener::MeshState>,
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