feat(mesh): ContentRef typed variant + send/fetch RPCs (Phase 3b)
Adds attachment sharing over the mesh: a ContentRef envelope (variant 19)
carries the blob CID, size, mime, optional thumb/caption, and a per-peer
HMAC capability URL so the recipient fetches the full blob out-of-band via
`GET {sender_onion}/blob/{cid}?cap=..&exp=..&peer=..`. BlobStore is shared
from ApiHandler into RpcHandler so mesh.send-content and mesh.fetch-content
(reqwest via TOR_SOCKS_PROXY) hit the same store and cap_key.
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
e8a729a4c7
commit
dce5084451
@@ -277,6 +277,22 @@ pub(super) async fn handle_typed_message(
|
||||
dispatch_tx_confirmation(&envelope, sender_contact_id, sender_name, state).await;
|
||||
}
|
||||
|
||||
Some(MeshMessageType::ContentRef) => {
|
||||
match message_types::decode_payload::<message_types::ContentRefPayload>(&envelope.v) {
|
||||
Ok(content) => {
|
||||
let text = match (&content.filename, &content.caption) {
|
||||
(Some(fname), Some(c)) => format!("📎 {} — {}", fname, c),
|
||||
(Some(fname), None) => format!("📎 {}", fname),
|
||||
(None, Some(c)) => format!("📎 {}", c),
|
||||
(None, None) => format!("📎 {} ({} bytes)", content.mime, content.size),
|
||||
};
|
||||
let json = payload_to_json(&content);
|
||||
store_typed_message(state, sender_contact_id, sender_name, &text, "content_ref", json, Some(envelope.seq)).await;
|
||||
}
|
||||
Err(e) => warn!("Failed to decode content_ref payload: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Some(MeshMessageType::Text) => {
|
||||
// Typed text message — extract and store as plain text
|
||||
let text = String::from_utf8_lossy(&envelope.v).to_string();
|
||||
|
||||
@@ -42,6 +42,8 @@ pub enum MeshMessageType {
|
||||
LightningRelayResponse = 11,
|
||||
/// Confirmation update for a relayed transaction (1, 2, 3 confs).
|
||||
TxConfirmation = 12,
|
||||
/// Attachment/file reference: CID of a blob held by the sender, fetched out-of-band.
|
||||
ContentRef = 19,
|
||||
}
|
||||
|
||||
impl MeshMessageType {
|
||||
@@ -60,6 +62,7 @@ impl MeshMessageType {
|
||||
10 => Some(Self::LightningRelay),
|
||||
11 => Some(Self::LightningRelayResponse),
|
||||
12 => Some(Self::TxConfirmation),
|
||||
19 => Some(Self::ContentRef),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -79,6 +82,7 @@ impl MeshMessageType {
|
||||
Self::LightningRelay => "lightning_relay",
|
||||
Self::LightningRelayResponse => "lightning_relay_response",
|
||||
Self::TxConfirmation => "tx_confirmation",
|
||||
Self::ContentRef => "content_ref",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -316,6 +320,28 @@ pub struct LightningRelayResponsePayload {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Content/attachment reference: points at a blob held by the sender that
|
||||
/// recipients fetch out-of-band via `GET {sender_onion}/blob/{cid}?cap=..&exp=..&peer=..`.
|
||||
/// Thumb bytes (≤60B) may be inlined for immediate display; full blob is lazy.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContentRefPayload {
|
||||
pub cid: String,
|
||||
pub size: u64,
|
||||
pub mime: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub filename: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub thumb_bytes: Option<Vec<u8>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub caption: Option<String>,
|
||||
/// Sender's onion host (no scheme, no trailing slash) — `<host>.onion`.
|
||||
pub sender_onion: String,
|
||||
/// HMAC capability token scoped to (cid, recipient_pubkey, exp).
|
||||
pub cap_token: String,
|
||||
/// Capability expiry (unix seconds).
|
||||
pub cap_exp: u64,
|
||||
}
|
||||
|
||||
/// Transaction confirmation update (relay node → originator).
|
||||
/// Sent after each new confirmation (1, 2, 3) until fully confirmed.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user