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
@@ -3,7 +3,7 @@ use crate::blobs::DEFAULT_CAP_TTL_SECS;
|
||||
use crate::mesh::message_types::{
|
||||
self, AlertPayload, AlertType, ChannelInvitePayload, ContentRefPayload, Coordinate,
|
||||
DeletePayload, EditPayload, ForwardPayload, InvoicePayload, MessageKey, MeshMessageType,
|
||||
PresencePayload, PsbtHashPayload, ReactionPayload, ReadReceiptPayload, ReplyPayload,
|
||||
PsbtHashPayload, ReactionPayload, ReadReceiptPayload, ReplyPayload,
|
||||
TypedEnvelope,
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -647,6 +647,10 @@ impl RpcHandler {
|
||||
let msg = svc
|
||||
.send_typed_wire(contact_id, wire, "read_receipt", &display, typed_json, seq)
|
||||
.await?;
|
||||
// Read receipts are control envelopes; the receiver uses them to
|
||||
// roll the ✓✓ marker forward on the matching outgoing bubble. They
|
||||
// must not surface as standalone bubbles in our own chat history.
|
||||
svc.drop_message_by_id(msg.id).await;
|
||||
info!(contact_id, seq, "Sent read receipt over mesh");
|
||||
Ok(serde_json::json!({ "sent": true, "message_id": msg.id, "sender_seq": seq }))
|
||||
}
|
||||
@@ -776,6 +780,10 @@ impl RpcHandler {
|
||||
let msg = svc
|
||||
.send_typed_wire(contact_id, wire, "edit", &new_text, typed_json, seq)
|
||||
.await?;
|
||||
// Edits are control envelopes — they mutate the target bubble in
|
||||
// apply_local_edit below, so the standalone Sent record has no UI
|
||||
// value and would just clutter the chat.
|
||||
svc.drop_message_by_id(msg.id).await;
|
||||
|
||||
// Best-effort: apply the edit to our own local copy too, so the UI
|
||||
// updates without waiting for an echo.
|
||||
@@ -824,6 +832,10 @@ impl RpcHandler {
|
||||
let msg = svc
|
||||
.send_typed_wire(contact_id, wire, "delete", "(deleted)", typed_json, seq)
|
||||
.await?;
|
||||
// Delete is a control envelope — apply_local_delete below tombstones
|
||||
// the target bubble in place, so the standalone Sent record is just
|
||||
// noise in the chat history.
|
||||
svc.drop_message_by_id(msg.id).await;
|
||||
|
||||
svc.apply_local_delete(target_seq).await;
|
||||
|
||||
@@ -836,35 +848,14 @@ impl RpcHandler {
|
||||
/// Params: `{ channel?, status? }`. Defaults: channel 0, status "online".
|
||||
pub(in crate::api::rpc) async fn handle_mesh_broadcast_presence(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
_params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.unwrap_or(serde_json::json!({}));
|
||||
let channel = params["channel"].as_u64().unwrap_or(0) as u8;
|
||||
let status = params["status"].as_str().unwrap_or("online").to_string();
|
||||
|
||||
let presence = PresencePayload {
|
||||
status: status.clone(),
|
||||
last_active: chrono::Utc::now().timestamp() as u32,
|
||||
};
|
||||
|
||||
let service = self.mesh_service.read().await;
|
||||
let svc = service
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Mesh service not running"))?;
|
||||
let seq = svc.next_send_seq(0).await;
|
||||
let payload = message_types::encode_payload(&presence)?;
|
||||
let envelope = TypedEnvelope::new(MeshMessageType::Presence, payload).with_seq(seq);
|
||||
let wire = envelope.to_wire()?;
|
||||
let typed_json = serde_json::to_value(&presence).ok();
|
||||
// Best-effort: if the mesh device isn't connected, skip silently —
|
||||
// presence heartbeats don't deserve a user-visible error.
|
||||
match svc
|
||||
.send_channel_typed_wire(channel, wire, "presence", &status, typed_json, seq)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Ok(serde_json::json!({ "sent": true, "sender_seq": seq })),
|
||||
Err(e) => Ok(serde_json::json!({ "sent": false, "reason": e.to_string() })),
|
||||
}
|
||||
// DISABLED: presence broadcasts were spamming the public channel
|
||||
// with malformed CBOR bytes (repeaters re-echoed our
|
||||
// PresencePayload as plaintext, producing "av�…fstatusfonline…").
|
||||
// The RPC stays registered so frontends that still call it don't
|
||||
// hard-fail, but it no longer transmits anything.
|
||||
Ok(serde_json::json!({ "sent": false, "reason": "presence disabled" }))
|
||||
}
|
||||
|
||||
/// mesh.presence-list — return the in-memory presence map (pubkey → status+timestamps).
|
||||
|
||||
Reference in New Issue
Block a user