fix(mesh): Meshtastic 3ccc pkc_capable pill + Sideband image interop + critical CBOR wire-bloat fix
Merges in the meshtastic agent's now-finished work alongside this session's
continuation: stock-peer (3ccc) PKI-capability is now stamped through
get_contacts -> refresh_contacts -> MeshPeer.pkc_capable, so a directed DM to/from
a PKC-capable stock Meshtastic peer correctly shows the E2E pill on the Sent row,
not just received messages. Confirmed live: .198 sees "Meshtastic 3ccc" with
pkc_capable=true.
Also fixes two real interop/correctness bugs found while live-testing the
Reticulum <-> Sideband link:
- Receive: the daemon only ever read LXMF's plain-text content, silently
dropping native FIELD_IMAGE/FIELD_FILE_ATTACHMENTS fields — a stock
Sideband/NomadNet photo vanished into a blank-space message. Now decoded
into the same ContentInline typed envelope our own attachments use.
- Send: images to a non-archy (stock) peer now use native LXMF FIELD_IMAGE
instead of our own opaque CBOR wire format, which Sideband can't decode.
- Root cause of a garbled MC-chunk-fragment bug: TypedEnvelope.v/.sig (the
OUTER wrapper every message type uses) serialized raw bytes as a CBOR
array-of-integers instead of a native byte string, bloating every
message on the wire ~2-3.5x — enough to push even a tiny ReadReceipt
over the 140-byte single-frame chunking threshold. Root-caused by
reading ciborium's deserializer source directly (deserialize_bytes only
works within its internal scratch buffer; deserialize_byte_buf streams
unbounded).
Frontend: consolidated the attach/record buttons into a single animated "+"
menu (was overflowing the compose row).
857/857 tests pass. Verified live across all 5 deploy-roster nodes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f54c853128
commit
0eb5c258f5
@@ -5,6 +5,7 @@ use crate::mesh::message_types::{
|
||||
Coordinate, DeletePayload, EditPayload, ForwardPayload, InvoicePayload, MeshMessageType,
|
||||
MessageKey, PsbtHashPayload, ReactionPayload, ReadReceiptPayload, ReplyPayload, TypedEnvelope,
|
||||
};
|
||||
use crate::mesh::types::radio_transport_label;
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
@@ -429,17 +430,6 @@ impl RpcHandler {
|
||||
.put(&bytes, &mime, filename.clone(), None, false)
|
||||
.await?;
|
||||
|
||||
let content = ContentInlinePayload {
|
||||
mime: mime.clone(),
|
||||
filename: filename.clone(),
|
||||
caption: caption.clone(),
|
||||
bytes,
|
||||
};
|
||||
let seq = svc.next_send_seq(contact_id).await;
|
||||
let payload = message_types::encode_payload(&content)?;
|
||||
let envelope = TypedEnvelope::new(MeshMessageType::ContentInline, payload).with_seq(seq);
|
||||
let wire = envelope.to_wire()?;
|
||||
|
||||
let display = match (&filename, &caption) {
|
||||
(Some(f), Some(c)) => format!("📎 {} — {}", f, c),
|
||||
(Some(f), None) => format!("📎 {}", f),
|
||||
@@ -447,7 +437,8 @@ impl RpcHandler {
|
||||
(None, None) => format!("📎 {} ({} bytes)", mime, meta.size),
|
||||
};
|
||||
// Render as a content_ref card on the sender side (UI already knows
|
||||
// how to draw it from cid + mime + filename + size).
|
||||
// how to draw it from cid + mime + filename + size) regardless of
|
||||
// which wire format actually goes out — this is a local-only mirror.
|
||||
let typed_json = serde_json::json!({
|
||||
"cid": meta.cid,
|
||||
"size": meta.size,
|
||||
@@ -456,27 +447,60 @@ impl RpcHandler {
|
||||
"caption": caption,
|
||||
"inline": true,
|
||||
});
|
||||
let seq = svc.next_send_seq(contact_id).await;
|
||||
|
||||
let msg = if use_resource_transfer {
|
||||
svc.send_content_resource(
|
||||
// A stock (non-archy) peer can't decode our typed-envelope wire
|
||||
// format — send images to them via LXMF's native FIELD_IMAGE
|
||||
// instead, so they actually see the photo (Sideband/NomadNet).
|
||||
let is_archy = svc.is_archy_peer(contact_id).await;
|
||||
let native_image = !is_archy
|
||||
&& device_type == crate::mesh::types::DeviceType::Reticulum
|
||||
&& mime.starts_with("image/");
|
||||
|
||||
let msg = if native_image {
|
||||
svc.send_native_image(contact_id, &mime, bytes, caption.clone())
|
||||
.await?;
|
||||
svc.record_sent_typed(
|
||||
contact_id,
|
||||
wire,
|
||||
"content_ref",
|
||||
&display,
|
||||
Some(typed_json),
|
||||
seq,
|
||||
Some(radio_transport_label(device_type).to_string()),
|
||||
true, // Reticulum/LXMF is unconditionally E2E on every send
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
} else {
|
||||
svc.send_typed_wire(
|
||||
contact_id,
|
||||
wire,
|
||||
"content_ref",
|
||||
&display,
|
||||
Some(typed_json),
|
||||
seq,
|
||||
)
|
||||
.await?
|
||||
let content = ContentInlinePayload {
|
||||
mime: mime.clone(),
|
||||
filename: filename.clone(),
|
||||
caption: caption.clone(),
|
||||
bytes,
|
||||
};
|
||||
let payload = message_types::encode_payload(&content)?;
|
||||
let envelope = TypedEnvelope::new(MeshMessageType::ContentInline, payload).with_seq(seq);
|
||||
let wire = envelope.to_wire()?;
|
||||
if use_resource_transfer {
|
||||
svc.send_content_resource(
|
||||
contact_id,
|
||||
wire,
|
||||
"content_ref",
|
||||
&display,
|
||||
Some(typed_json),
|
||||
seq,
|
||||
)
|
||||
.await?
|
||||
} else {
|
||||
svc.send_typed_wire(
|
||||
contact_id,
|
||||
wire,
|
||||
"content_ref",
|
||||
&display,
|
||||
Some(typed_json),
|
||||
seq,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
|
||||
Reference in New Issue
Block a user