fix(mesh): DM-via-channel tunnel + disable presence spam

Meshcore direct unicast silently drops between our two Archy nodes
(firmware reports flood sends with resp_code=6 but nothing arrives).
Wrap DMs as channel-1 broadcasts with a [0xD1][dest_prefix(6)][inner]
header; receivers filter by prefix and dispatch the inner payload
through the existing typed/base64/chunk ladder. Shrink chunk body to
125B so the wrapper still fits the 160B LoRa budget. Auto-heal
routing: CMD_RESET_PATH (0x0D) any type-1 contact with path_len=0 on
refresh so floods take over. send_text now returns the firmware's
flood/direct mode flag for diagnostics.

Disable the 120s presence heartbeat broadcaster — its CBOR payload
was being re-echoed as plaintext by the shared repeater, spamming
every visible node with garbled "Archy-…: av�…fstatusfonline…"
messages on channel 0. mesh.broadcast-presence RPC stays registered
but no longer transmits. Re-enable only once presence moves off the
shared broadcast path.

Also: MeshState.cmd_tx behind RwLock so stop()→start() cycles don't
fail with "command channel already consumed"; MeshService.send_cmd
helper; drop_message_by_id for control envelopes that shouldn't
appear as Sent bubbles; self_advert_name reflected into MeshStatus
after set; path_len/flags parsed out of RESP_CONTACT.

Frontend: unified inbox merges mesh peers with federation nodes by
DID/pubkey/name; hide presence/read_receipt/edit/channel_invite/
contact_card from chat stream; publicChannel index → 1 to match the
new DM-via-channel routing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-14 10:24:27 -04:00
co-authored by Claude Opus 4.6
parent bdacc06a2b
commit d514e0e5e4
10 changed files with 771 additions and 111 deletions
+42 -18
View File
@@ -196,6 +196,7 @@ pub struct MeshService {
listener_handle: Option<tokio::task::JoinHandle<()>>,
deadman_handle: Option<tokio::task::JoinHandle<()>>,
block_announcer_handle: Option<tokio::task::JoinHandle<()>>,
presence_handle: Option<tokio::task::JoinHandle<()>>,
cmd_rx: Option<tokio::sync::mpsc::Receiver<listener::MeshCommand>>,
// Crypto identity for this node
our_did: String,
@@ -268,6 +269,7 @@ impl MeshService {
listener_handle: None,
deadman_handle: None,
block_announcer_handle: None,
presence_handle: None,
cmd_rx: Some(cmd_rx),
our_did: did.to_string(),
our_ed_pubkey_hex: ed_pubkey_hex.to_string(),
@@ -323,7 +325,7 @@ impl MeshService {
error!("Dead man's switch TRIGGERED — broadcasting alert");
if let Ok(wire) = dms.build_signed_alert(&dms_key).await {
for ch in [0u8, 1] {
let _ = dms_state.cmd_tx.send(
let _ = dms_state.send_cmd(
listener::MeshCommand::BroadcastChannel {
channel: ch,
payload: wire.clone(),
@@ -407,7 +409,7 @@ impl MeshService {
if pk_bytes.len() >= 6 {
let mut prefix = [0u8; 6];
prefix.copy_from_slice(&pk_bytes[..6]);
let _ = bha_state.cmd_tx.send(
let _ = bha_state.send_cmd(
listener::MeshCommand::SendRaw {
dest_pubkey_prefix: prefix,
payload: wire.clone(),
@@ -427,7 +429,7 @@ impl MeshService {
if pk_bytes.len() >= 6 {
let mut prefix = [0u8; 6];
prefix.copy_from_slice(&pk_bytes[..6]);
let _ = bha_state.cmd_tx.send(
let _ = bha_state.send_cmd(
listener::MeshCommand::SendRaw {
dest_pubkey_prefix: prefix,
payload: wire.clone(),
@@ -464,6 +466,16 @@ impl MeshService {
info!("Block header announcer started");
}
// Presence heartbeat broadcaster is DISABLED. The CBOR-encoded
// PresencePayload was rendering as garbled bytes on peers that
// didn't understand the typed envelope (e.g. the FreeMadeira
// repeater echoed it back as plaintext on channel 0), spamming
// every visible node with "Archy-…: av…fstatusfonline…" every
// 120s. Re-enable only after either (a) presence moves to a
// non-broadcast path or (b) we can guarantee no plain-text-only
// receivers on the shared channel.
self.presence_handle = None;
info!("Mesh service started");
Ok(())
}
@@ -484,6 +496,19 @@ impl MeshService {
handle.abort();
let _ = handle.await;
}
if let Some(handle) = self.presence_handle.take() {
handle.abort();
let _ = handle.await;
}
// Recreate the cmd channel so a subsequent start() has a fresh
// receiver. The listener task took ownership of the old receiver
// on its previous run and dropped it when the task ended, so
// without this swap the next start() hits "Command channel
// already consumed". Swapping the sender inside MeshState means
// every Arc holder transparently picks up the new channel.
let (new_tx, new_rx) = tokio::sync::mpsc::channel(32);
*self.state.cmd_tx.write().await = new_tx;
self.cmd_rx = Some(new_rx);
info!("Mesh service stopped");
}
@@ -583,9 +608,7 @@ impl MeshService {
let end = (start + MAX_CHUNK_B64).min(b64.len());
let chunk = &b64[start..end];
let frame = format!("MC{:02X}{:02X}{:02X}{}", msg_id, chunk_idx, total_chunks, chunk);
self.state
.cmd_tx
.send(listener::MeshCommand::SendText {
self.state.send_cmd(listener::MeshCommand::SendText {
dest_pubkey_prefix: dest_prefix,
payload: frame.into_bytes(),
})
@@ -621,9 +644,7 @@ impl MeshService {
let dest_prefix = self.peer_dest_prefix(contact_id).await?;
self.state
.cmd_tx
.send(listener::MeshCommand::SendText {
self.state.send_cmd(listener::MeshCommand::SendText {
dest_pubkey_prefix: dest_prefix,
payload,
})
@@ -841,6 +862,15 @@ impl MeshService {
messages.iter().find(|m| m.id == id).cloned()
}
/// Drop a stored MeshMessage by local id. Used after sending control
/// envelopes (read receipts) so they don't surface as their own
/// bubbles in the chat history. The wire frame is already on its way;
/// this just prunes the local Sent record.
pub async fn drop_message_by_id(&self, id: u64) {
let mut messages = self.state.messages.write().await;
messages.retain(|m| m.id != id);
}
/// Apply an Edit locally to any own-Sent message matching `sender_seq`
/// (sender_pubkey is implicit = self). Rewrites `plaintext` and appends
/// an `edited_at` marker on `typed_payload` so the UI can show "(edited)".
@@ -905,9 +935,7 @@ impl MeshService {
);
}
self.state
.cmd_tx
.send(listener::MeshCommand::BroadcastChannel {
self.state.send_cmd(listener::MeshCommand::BroadcastChannel {
channel,
payload: wire,
})
@@ -1014,9 +1042,7 @@ impl MeshService {
}
// Send through the listener's command channel
self.state
.cmd_tx
.send(listener::MeshCommand::BroadcastChannel {
self.state.send_cmd(listener::MeshCommand::BroadcastChannel {
channel,
payload,
})
@@ -1060,9 +1086,7 @@ impl MeshService {
}
drop(status);
self.state
.cmd_tx
.send(listener::MeshCommand::SendAdvert)
self.state.send_cmd(listener::MeshCommand::SendAdvert)
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;