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:
co-authored by
Claude Opus 4.6
parent
bdacc06a2b
commit
d514e0e5e4
@@ -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"))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user