fix(mesh): don't offer radio-only resource transfer to radio-unreachable peers

The federation fallback in the plain content-inline path wasn't enough —
mesh.transport-advice recommended the "resource-mesh" tier purely from our
own device being Reticulum-capable, without checking that THIS peer
actually has a radio route. For a federation-only contact (no radio twin)
that steered the frontend into send-content-inline's Reticulum
resource-transfer path, which has no dest_prefix to send to and fails with
"Peer is federation-only (no radio twin)" — reproduced after deploying the
first fix on a live node.

Adds MeshService::has_radio_route(contact_id), and gates both the
"resource-mesh" tier in mesh.transport-advice and the resource-transfer
branch in mesh.send-content-inline on it. Federation-only peers now fall
through to the has_tor branches, which route the frontend to
mesh.send-content (already correctly federation-aware) instead.

Landed from PR #133 (re-committed to drop private host details from the
original message; content identical).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 13:18:08 -04:00
committed by archipelago
co-authored by Claude Sonnet 5
parent a9e52fa310
commit 92111385b7
2 changed files with 33 additions and 2 deletions
+13
View File
@@ -1222,6 +1222,19 @@ impl MeshService {
Ok(dest_prefix)
}
/// True if `contact_id` is reachable over the mesh radio right now — the
/// same peer/twin resolution `peer_dest_prefix` performs, exposed as a
/// cheap bool so RPC handlers can gate radio-only transports (LXMF
/// native image, Reticulum resource transfer) without duplicating the
/// twin-resolution logic. A federation-only contact_id with no matching
/// radio twin returns false here — offering "resource-mesh" or native
/// image to such a peer sends it straight into `peer_dest_prefix`'s
/// "federation-only (no radio twin)" error (picture-send from a
/// federation-only contact, 2026-08-07).
pub async fn has_radio_route(&self, contact_id: u32) -> bool {
self.peer_dest_prefix(contact_id).await.is_ok()
}
/// Split an oversized wire payload into MC-framed base64 chunks and send
/// each via the mesh device. Matches the receive-side reassembly in
/// `mesh/listener/decode.rs::handle_chunked_frame` (header `MCIIXXTT`,