feat(peers): bidirectional /network peer requests
Before: Alice sent /network.send-request to Bob, Bob accepted via
/network.accept-request and gained Alice in his peers list, but Alice
was never notified — her pending row sat there and she had to
manually add Bob separately. User complaint: "it's strange you have
to do it both ways."
Fix — the accept now fires a best-effort connection_accepted message
back to the requester:
- handle_network_accept_request: after writing the local peer record,
assembles a `{type: "connection_accepted", request_id, from_did,
from_onion, from_pubkey}` JSON, signs + encrypts + POSTs it to the
requester via node_message::send_to_peer. Uses PeerRequest internally
so it prefers FIPS and falls back to Tor.
- handle_node_message: parses incoming plaintext as JSON; on a match
for type=connection_accepted, auto-adds the sender to peers.json
(the existing self-pubkey guard in add_peer still applies) and
short-circuits the normal store_received path so the acceptance
doesn't also land as a chat message in Alice's inbox.
Offline handling: if Alice is offline when Bob accepts, the notify
warns and the local accept still succeeds. Alice will receive any
subsequent message from Bob normally; future iteration could
retry on reconnect.
Federation-invite flow (federation.accept-invite → notify_join) was
already bidirectional; this closes the gap for the peer flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,47 @@ impl ApiHandler {
|
||||
msg.clone()
|
||||
};
|
||||
|
||||
// Detect a `connection_accepted` reply: the remote peer just
|
||||
// approved an outbound request we sent, so mirror their add on
|
||||
// our side (bidirectional peering without a manual second
|
||||
// click). JSON-shape only — any non-matching payload stays in
|
||||
// the normal received-messages store below.
|
||||
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&plaintext) {
|
||||
if val.get("type").and_then(|v| v.as_str()) == Some("connection_accepted") {
|
||||
if let (Some(their_onion), Some(their_pubkey)) = (
|
||||
val.get("from_onion").and_then(|v| v.as_str()),
|
||||
val.get("from_pubkey").and_then(|v| v.as_str()),
|
||||
) {
|
||||
let data_dir = std::path::Path::new("/var/lib/archipelago");
|
||||
let peer = crate::peers::KnownPeer {
|
||||
onion: their_onion.to_string(),
|
||||
pubkey: their_pubkey.to_string(),
|
||||
name: val
|
||||
.get("from_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from),
|
||||
added_at: Some(chrono::Utc::now().to_rfc3339()),
|
||||
};
|
||||
match crate::peers::add_peer(data_dir, peer).await {
|
||||
Ok(_) => tracing::info!(
|
||||
from = %sanitize_log_string(from),
|
||||
"Auto-added peer after connection_accepted"
|
||||
),
|
||||
Err(e) => tracing::warn!(
|
||||
from = %sanitize_log_string(from),
|
||||
error = %e,
|
||||
"Failed to auto-add peer on connection_accepted"
|
||||
),
|
||||
}
|
||||
}
|
||||
return Ok(build_response(
|
||||
StatusCode::OK,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"ok":true,"handled":"connection_accepted"}"#),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let safe_from = sanitize_log_string(from);
|
||||
let safe_msg = sanitize_log_string(&plaintext);
|
||||
tracing::info!("Received message from {}: {}", safe_from, safe_msg);
|
||||
|
||||
@@ -160,7 +160,9 @@ impl RpcHandler {
|
||||
Ok(serde_json::json!({ "requests": requests }))
|
||||
}
|
||||
|
||||
/// Accept a connection request — add peer to trusted list.
|
||||
/// Accept a connection request — add peer to trusted list AND send
|
||||
/// a `connection_accepted` notification back to the requester so
|
||||
/// their side auto-adds us without a second manual round-trip.
|
||||
pub(super) async fn handle_network_accept_request(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
@@ -175,7 +177,8 @@ impl RpcHandler {
|
||||
let req = requests
|
||||
.iter()
|
||||
.find(|r| r.id == request_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Request not found: {}", request_id))?;
|
||||
.ok_or_else(|| anyhow::anyhow!("Request not found: {}", request_id))?
|
||||
.clone();
|
||||
|
||||
// Add to known peers
|
||||
let peer = peers::KnownPeer {
|
||||
@@ -189,6 +192,47 @@ impl RpcHandler {
|
||||
// Remove the request
|
||||
self.delete_request(request_id).await?;
|
||||
|
||||
// Notify the requester we've accepted so their UI auto-adds us and
|
||||
// clears its outbound pending row. Best-effort — if the peer is
|
||||
// offline we don't fail the accept; the next connection_request
|
||||
// retry on their side will resolve eventually.
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let my_pubkey = data.server_info.pubkey.clone();
|
||||
let my_did = crate::identity::did_key_from_pubkey_hex(&my_pubkey).ok();
|
||||
let my_onion = crate::container::docker_packages::read_tor_address("archipelago")
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let accept_msg = serde_json::json!({
|
||||
"type": "connection_accepted",
|
||||
"request_id": request_id,
|
||||
"from_did": my_did,
|
||||
"from_onion": my_onion,
|
||||
"from_pubkey": my_pubkey,
|
||||
});
|
||||
let to_fips_npub =
|
||||
crate::federation::fips_npub_for_onion(&self.config.data_dir, &req.from_onion).await;
|
||||
let identity_dir = self.config.data_dir.join("identity");
|
||||
let signing_key = crate::identity::NodeIdentity::load_or_create(&identity_dir)
|
||||
.await
|
||||
.ok();
|
||||
if let Err(e) = crate::node_message::send_to_peer(
|
||||
&req.from_onion,
|
||||
to_fips_npub.as_deref(),
|
||||
&my_pubkey,
|
||||
&accept_msg.to_string(),
|
||||
signing_key.as_ref().map(|i| i.signing_key()),
|
||||
Some(&req.from_pubkey),
|
||||
data.server_info.name.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
to = %req.from_did,
|
||||
error = %e,
|
||||
"connection_accepted notify failed (requester will still be able to see us on their next retry)"
|
||||
);
|
||||
}
|
||||
|
||||
tracing::info!("Accepted connection from {}", req.from_did);
|
||||
Ok(serde_json::json!({ "ok": true }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user