1400 lines
56 KiB
Rust
Raw Normal View History

//! Mesh networking: LoRa radio integration for offline peer discovery
2026-03-17 00:03:08 +00:00
//! and encrypted messaging between Archipelago nodes.
//!
//! Supports Meshcore firmware via Companion USB and Meshtastic firmware via
//! the Meshtastic serial API on Heltec, T-Beam, RAK WisBlock, Station G2,
//! and other ESP32/nRF52-based LoRa boards.
2026-03-17 00:03:08 +00:00
pub mod alerts;
pub mod bitcoin_relay;
2026-03-17 00:03:08 +00:00
pub mod crypto;
pub mod listener;
pub mod meshtastic;
pub mod message_types;
pub mod outbox;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
pub mod protocol;
pub mod ratchet;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
pub mod serial;
pub mod session;
pub mod steganography;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
pub mod types;
pub mod x3dh;
2026-03-17 00:03:08 +00:00
pub use types::*;
use alerts::DeadManSwitch;
2026-03-17 00:03:08 +00:00
use anyhow::{Context, Result};
use bitcoin_relay::{BlockHeaderCache, RelayTracker};
2026-03-17 00:03:08 +00:00
use ed25519_dalek::SigningKey;
use listener::MeshState;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
2026-03-17 00:03:08 +00:00
use tokio::fs;
use tokio::sync::watch;
use tracing::{error, info, warn};
2026-03-17 00:03:08 +00:00
const MESH_CONFIG_FILE: &str = "mesh-config.json";
const MESH_IGNORED_RADIO_FILE: &str = "mesh-ignored-radio-contacts.json";
2026-03-17 00:03:08 +00:00
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Derive a stable synthetic `contact_id` for a federation peer from its
/// archipelago ed25519 pubkey. Mesh LoRa contacts use meshcore firmware's
/// own ID space (small ints 0..N), so federation peers are mapped into the
/// high half of u32 space to avoid collision. Both the receive path
/// (`inject_typed_from_federation`) and the startup pre-seed use this
/// formula so they always produce the same id for the same peer.
pub(crate) fn federation_peer_contact_id(archipelago_pubkey_hex: &str) -> u32 {
let bytes = hex::decode(archipelago_pubkey_hex).unwrap_or_default();
if bytes.len() < 4 {
return 0x8000_0001;
}
let low = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
0x8000_0000 | (low & 0x7FFF_FFFF)
}
/// Upsert a mesh peer record representing a federation node so the UI can
/// address it as a chat and `mesh.send-content` can route ContentRef to it.
/// Existing entries (same contact_id) are updated in place, preserving any
/// previously observed radio state (rssi/snr/hops).
pub(crate) async fn upsert_federation_peer(
state: &Arc<listener::MeshState>,
archipelago_pubkey_hex: &str,
did: &str,
name: Option<&str>,
) -> u32 {
let contact_id = federation_peer_contact_id(archipelago_pubkey_hex);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let display_name = name.map(|s| s.to_string()).unwrap_or_else(|| {
let short = &archipelago_pubkey_hex[..archipelago_pubkey_hex.len().min(8)];
format!("Archipelago {}", short)
});
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let mut peers = state.peers.write().await;
let existing = peers.get(&contact_id).cloned();
let peer = MeshPeer {
contact_id,
advert_name: display_name,
did: Some(did.to_string()),
pubkey_hex: Some(archipelago_pubkey_hex.to_string()),
x25519_pubkey: existing.as_ref().and_then(|p| p.x25519_pubkey),
rssi: existing.as_ref().and_then(|p| p.rssi),
snr: existing.as_ref().and_then(|p| p.snr),
last_heard: chrono::Utc::now().to_rfc3339(),
hops: existing.as_ref().map(|p| p.hops).unwrap_or(0),
};
peers.insert(contact_id, peer);
drop(peers);
state.update_peer_count().await;
contact_id
}
/// Load federation nodes from disk and upsert each as a synthetic mesh peer.
/// Called at MeshService startup so the chat list already contains every
/// known federation node — users can share files to them without first
/// receiving a message.
pub(crate) async fn seed_federation_peers_into_mesh(
state: &Arc<listener::MeshState>,
data_dir: &Path,
) {
let nodes = match crate::federation::load_nodes(data_dir).await {
Ok(n) => n,
Err(_) => return,
};
for node in nodes {
upsert_federation_peer(state, &node.pubkey, &node.did, node.name.as_deref()).await;
}
}
2026-03-17 00:03:08 +00:00
/// Mesh configuration (persisted to disk).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeshConfig {
pub enabled: bool,
/// Specific device path, or None for auto-detection.
#[serde(default)]
pub device_path: Option<String>,
/// Channel name for broadcasts.
#[serde(default)]
pub channel_name: Option<String>,
/// Whether to periodically broadcast our identity.
#[serde(default)]
pub broadcast_identity: bool,
/// Custom advertised name on the mesh network.
#[serde(default)]
pub advert_name: Option<String>,
/// Off-grid mode: disable Tor/internet, route everything via mesh only.
#[serde(default)]
pub mesh_only_mode: Option<bool>,
/// Announce new Bitcoin block headers over mesh (internet-connected nodes only).
#[serde(default)]
pub announce_block_headers: bool,
/// Steganographic encoding mode for mesh messages (Normal = disabled).
#[serde(default)]
pub steganography_mode: steganography::SteganographyMode,
/// Encrypt directed relay messages (TX, Lightning, block headers) via ratchet or shared secret.
/// Set to false to disable encryption for debugging or rollback.
#[serde(default = "default_true")]
pub encrypt_relay_messages: bool,
}
fn default_true() -> bool {
true
2026-03-17 00:03:08 +00:00
}
impl Default for MeshConfig {
fn default() -> Self {
Self {
enabled: false,
device_path: None,
channel_name: Some("archipelago".to_string()),
broadcast_identity: true,
advert_name: None,
mesh_only_mode: None,
announce_block_headers: false,
steganography_mode: steganography::SteganographyMode::Normal,
encrypt_relay_messages: true,
2026-03-17 00:03:08 +00:00
}
}
}
pub async fn load_config(data_dir: &Path) -> Result<MeshConfig> {
let path = data_dir.join(MESH_CONFIG_FILE);
if !path.exists() {
return Ok(MeshConfig::default());
}
let content = fs::read_to_string(&path)
.await
.context("Failed to read mesh config")?;
let config: MeshConfig = serde_json::from_str(&content).unwrap_or_default();
Ok(config)
}
pub async fn save_config(data_dir: &Path, config: &MeshConfig) -> Result<()> {
fs::create_dir_all(data_dir)
.await
.context("Failed to create data dir")?;
let content =
serde_json::to_string_pretty(config).context("Failed to serialize mesh config")?;
fs::write(data_dir.join(MESH_CONFIG_FILE), content)
.await
.context("Failed to write mesh config")?;
Ok(())
}
pub async fn load_ignored_radio_contacts(data_dir: &Path) -> Vec<String> {
let path = data_dir.join(MESH_IGNORED_RADIO_FILE);
if !path.exists() {
return Vec::new();
}
match fs::read_to_string(&path).await {
Ok(s) => serde_json::from_str::<Vec<String>>(&s).unwrap_or_default(),
Err(_) => Vec::new(),
}
}
pub async fn save_ignored_radio_contacts(data_dir: &Path, pubkeys: &[String]) -> Result<()> {
fs::create_dir_all(data_dir).await.ok();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let content =
serde_json::to_string_pretty(pubkeys).context("Failed to serialize ignored-radio list")?;
fs::write(data_dir.join(MESH_IGNORED_RADIO_FILE), content)
.await
.context("Failed to write ignored-radio list")?;
Ok(())
}
2026-03-17 00:03:08 +00:00
/// Detect serial devices that could be mesh radios.
/// Checks both Meshcore (via probe) and legacy Meshtastic paths.
pub async fn detect_devices() -> Vec<String> {
serial::detect_serial_devices().await
}
// ─── MeshService ────────────────────────────────────────────────────────
/// Top-level mesh networking service.
/// Manages the background listener, exposes APIs for RPC handlers.
pub struct MeshService {
state: Arc<MeshState>,
config: MeshConfig,
data_dir: PathBuf,
shutdown_tx: Option<watch::Sender<bool>>,
listener_handle: Option<tokio::task::JoinHandle<()>>,
deadman_handle: Option<tokio::task::JoinHandle<()>>,
block_announcer_handle: Option<tokio::task::JoinHandle<()>>,
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>
2026-04-14 10:24:27 -04:00
presence_handle: Option<tokio::task::JoinHandle<()>>,
2026-03-17 00:03:08 +00:00
cmd_rx: Option<tokio::sync::mpsc::Receiver<listener::MeshCommand>>,
// Crypto identity for this node
our_did: String,
our_ed_pubkey_hex: String,
our_x25519_secret: [u8; 32],
our_x25519_pubkey_hex: String,
signing_key: SigningKey,
/// Human-readable server name (e.g. "Arch Dev", "ThinkPad") for mesh adverts.
server_name: Option<String>,
// Phase 4: off-grid Bitcoin operations
pub block_header_cache: Arc<BlockHeaderCache>,
pub relay_tracker: Arc<RelayTracker>,
pub dead_man_switch: Arc<DeadManSwitch>,
2026-03-17 00:03:08 +00:00
}
impl MeshService {
/// Create a new MeshService. Does not start the listener yet.
pub async fn new(
data_dir: &Path,
signing_key: &SigningKey,
did: &str,
ed_pubkey_hex: &str,
) -> Result<Self> {
let config = load_config(data_dir).await?;
let channel_name = config
.channel_name
.clone()
.unwrap_or_else(|| "archipelago".to_string());
let block_header_cache = Arc::new(BlockHeaderCache::new());
let relay_tracker = Arc::new(RelayTracker::new());
let session_manager = Arc::new(session::SessionManager::new(data_dir));
let (state, _rx, cmd_rx) = MeshState::new(
&channel_name,
Arc::clone(&block_header_cache),
Some(Arc::clone(&relay_tracker)),
config.steganography_mode,
config.encrypt_relay_messages,
Arc::clone(&session_manager),
feat(mesh): Telegram primitives pass + attachment transport router Bundles the Phase 2b/3/4/5 work that accumulated across prior sessions and the new attachment chunking router from this session. Everything ships in one shot so the full mesh surface stays coherent on-wire. Telegram primitives (variants 13–18, 20–22): - Reply / Reaction / ReadReceipt / Forward / Edit / Delete - Presence heartbeat + last-seen tracking - ChannelInvite + ContactCard payload types - MessageKey (sender_pubkey, sender_seq) as cross-transport identity - Action menu, reply banner, edit banner, tombstones, (edited) marker - Debounced auto-read-receipts on scroll + message arrival Activated prototypes (Phase 4): - PsbtHash send RPC - Contacts CRUD (in-memory alias/notes/pinned/blocked) - Outbox 📤 badge, rotate-prekeys button - Chunked send fallback (MCIIXXTT framing) as auto-failover inside send_typed_wire when a typed wire exceeds the LoRa per-frame budget Unified inbox (Phase 1): - conversations.list + conversations.messages RPCs (UI collapse deferred) Attachment transport router (new this session): - ContentInline variant 23 + ContentInlinePayload carrying file bytes directly in the envelope for small files with no Tor path - mesh.send-content-inline RPC — mirrors to local BlobStore, rides send_typed_wire which auto-chunks over MCIIXXTT framing (~2.3 KB cap) - mesh.transport-advice RPC as single source of truth for tier decisions: auto-mesh / choose / tor-only / impossible - Receive arm writes inline bytes to local BlobStore so the existing content_ref card renderer handles both transports uniformly - MeshState.blob_store field + order-independent propagation from RpcHandler::set_blob_store / set_mesh_service - Frontend handleAttachFile calls advice first, branches into silent auto-send, transport-chooser modal, Tor-only path, or red error - Transport modal with 📡 mesh / 🧅 Tor options + ETA + disabled state when peer has no Tor reachability Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 20:40:19 -04:00
ed_pubkey_hex.to_string(),
);
2026-03-17 00:03:08 +00:00
// Derive X25519 keys from Ed25519 identity
let x25519_secret = crypto::ed25519_secret_to_x25519(signing_key);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let x25519_pubkey =
crypto::ed25519_pubkey_to_x25519(&signing_key.verifying_key().to_bytes())?;
2026-03-17 00:03:08 +00:00
let x25519_pubkey_hex = hex::encode(x25519_pubkey);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let dead_man_switch = Arc::new(DeadManSwitch::new(data_dir).await.unwrap_or_else(|e| {
warn!("Failed to load dead man config (using defaults): {}", e);
// Fallback: create with defaults (won't persist until configured)
tokio::runtime::Handle::current()
.block_on(DeadManSwitch::new(data_dir))
.expect("DeadManSwitch fallback should succeed")
}));
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// Pre-seed mesh state with a synthetic peer record for every known
// federation node. Mesh LoRa discovery and federation have disjoint
// identity namespaces, so without this step a user can't address a
// federated peer from the mesh UI until one side receives over the
// radio — which never happens for nodes that only share Tor.
seed_federation_peers_into_mesh(&state, data_dir).await;
// Load the radio-contact blocklist so previously-wiped firmware
// contacts stay hidden after restart. Without this, meshcore's
// persistent on-device contact table regenerates the rows on the
// next refresh_contacts cycle and the user sees stale entries
// they already cleared.
{
let ignored = load_ignored_radio_contacts(data_dir).await;
let mut set = state.radio_contact_blocklist.write().await;
for pk in ignored {
set.insert(pk);
}
}
2026-03-17 00:03:08 +00:00
Ok(Self {
state,
config,
data_dir: data_dir.to_path_buf(),
shutdown_tx: None,
listener_handle: None,
deadman_handle: None,
block_announcer_handle: None,
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>
2026-04-14 10:24:27 -04:00
presence_handle: None,
2026-03-17 00:03:08 +00:00
cmd_rx: Some(cmd_rx),
our_did: did.to_string(),
our_ed_pubkey_hex: ed_pubkey_hex.to_string(),
our_x25519_secret: x25519_secret,
our_x25519_pubkey_hex: x25519_pubkey_hex,
signing_key: signing_key.clone(),
server_name: None,
block_header_cache,
relay_tracker,
dead_man_switch,
2026-03-17 00:03:08 +00:00
})
}
/// Set the human-readable server name used in mesh adverts.
pub fn set_server_name(&mut self, name: Option<String>) {
self.server_name = name;
}
2026-03-17 00:03:08 +00:00
/// Start the background mesh listener.
pub fn start(&mut self) -> Result<()> {
if self.listener_handle.is_some() {
anyhow::bail!("Mesh listener already running");
}
let (shutdown_tx, shutdown_rx) = watch::channel(false);
self.shutdown_tx = Some(shutdown_tx);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let cmd_rx = self
.cmd_rx
.take()
2026-03-17 00:03:08 +00:00
.ok_or_else(|| anyhow::anyhow!("Command channel already consumed"))?;
let handle = listener::spawn_mesh_listener(
Arc::clone(&self.state),
self.config.device_path.clone(),
self.our_did.clone(),
self.our_ed_pubkey_hex.clone(),
self.our_x25519_secret,
self.our_x25519_pubkey_hex.clone(),
self.server_name.clone(),
2026-03-17 00:03:08 +00:00
shutdown_rx,
cmd_rx,
);
self.listener_handle = Some(handle);
// Spawn dead man's switch background checker
let dms = Arc::clone(&self.dead_man_switch);
let dms_state = Arc::clone(&self.state);
let dms_key = self.signing_key.clone();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let dms_shutdown = self
.shutdown_tx
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Shutdown channel not initialized"))?
.subscribe();
let dms_handle = tokio::spawn(async move {
let mut shutdown = dms_shutdown;
let mut interval = tokio::time::interval(Duration::from_secs(60));
interval.tick().await; // skip first immediate tick
loop {
tokio::select! {
_ = interval.tick() => {
if dms.is_triggered().await {
let was_triggered = *dms.triggered_flag().await;
if !was_triggered {
error!("Dead man's switch TRIGGERED — broadcasting alert");
if let Ok(wire) = dms.build_signed_alert(&dms_key).await {
for ch in [0u8, 1] {
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>
2026-04-14 10:24:27 -04:00
let _ = dms_state.send_cmd(
listener::MeshCommand::BroadcastChannel {
channel: ch,
payload: wire.clone(),
},
).await;
}
}
dms.mark_triggered().await;
}
}
}
_ = shutdown.changed() => {
if *shutdown.borrow() { return; }
}
}
}
});
self.deadman_handle = Some(dms_handle);
// Spawn block header announcer (internet-connected nodes only)
if self.config.announce_block_headers {
let bha_state = Arc::clone(&self.state);
let bha_cache = Arc::clone(&self.block_header_cache);
let bha_key = self.signing_key.clone();
let bha_did = self.our_did.clone();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let bha_shutdown = self
.shutdown_tx
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Shutdown channel not initialized"))?
.subscribe();
let bha_handle = tokio::spawn(async move {
let mut shutdown = bha_shutdown;
let mut interval = tokio::time::interval(Duration::from_secs(30));
interval.tick().await; // skip first
let mut last_announced_height: u64 = 0;
let client = match reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
{
Ok(c) => c,
Err(e) => {
error!("Failed to create HTTP client for block announcer: {}", e);
return;
}
};
loop {
tokio::select! {
_ = interval.tick() => {
// Poll Bitcoin Core for latest block
match bitcoin_rpc_getblockcount(&client).await {
Ok(height) if height > last_announced_height => {
if let Ok(header) = bitcoin_rpc_getblockheader_by_height(&client, height).await {
// Store in cache
let payload = message_types::BlockHeaderPayload {
height,
hash: header.hash.clone(),
prev_hash: header.prev_hash.clone(),
timestamp: header.timestamp,
announced_by: bha_did.clone(),
};
let _ = bha_cache.store_header(payload).await;
// Build signed announcement and broadcast
match bitcoin_relay::build_block_header_announcement(
height,
&header.hash,
&header.prev_hash,
header.timestamp,
&bha_did,
&bha_key,
) {
Ok(wire) => {
// Send to peers — prefer Archy nodes, fall back to all (max 5)
let peers = bha_state.peers.read().await;
let mut sent = 0u32;
let max_peers = 5u32;
// First pass: Archy nodes
for peer in peers.values() {
if sent >= max_peers { break; }
if !peer.advert_name.starts_with("Archy-") { continue; }
if let Some(ref pk) = peer.pubkey_hex {
if let Ok(pk_bytes) = hex::decode(pk) {
if pk_bytes.len() >= 6 {
let mut prefix = [0u8; 6];
prefix.copy_from_slice(&pk_bytes[..6]);
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>
2026-04-14 10:24:27 -04:00
let _ = bha_state.send_cmd(
listener::MeshCommand::SendRaw {
dest_pubkey_prefix: prefix,
payload: wire.clone(),
},
).await;
sent += 1;
}
}
}
}
// Second pass: any peer if no Archy nodes found
if sent == 0 {
for peer in peers.values() {
if sent >= max_peers { break; }
if let Some(ref pk) = peer.pubkey_hex {
if let Ok(pk_bytes) = hex::decode(pk) {
if pk_bytes.len() >= 6 {
let mut prefix = [0u8; 6];
prefix.copy_from_slice(&pk_bytes[..6]);
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>
2026-04-14 10:24:27 -04:00
let _ = bha_state.send_cmd(
listener::MeshCommand::SendRaw {
dest_pubkey_prefix: prefix,
payload: wire.clone(),
},
).await;
sent += 1;
}
}
}
}
}
drop(peers);
last_announced_height = height;
info!(height, hash = %header.hash, peers = sent, "Announced block header to Archy peers");
}
Err(e) => warn!("Failed to build block announcement: {}", e),
}
}
}
Ok(_) => {} // No new block
Err(e) => {
// Bitcoin not running or not reachable — that's fine, skip
tracing::debug!("Block poll: {}", e);
}
}
}
_ = shutdown.changed() => {
if *shutdown.borrow() { return; }
}
}
}
});
self.block_announcer_handle = Some(bha_handle);
info!("Block header announcer started");
}
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>
2026-04-14 10:24:27 -04:00
// 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<61>…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;
2026-03-17 00:03:08 +00:00
info!("Mesh service started");
Ok(())
}
/// Stop the background listener and dead man's switch.
2026-03-17 00:03:08 +00:00
pub async fn stop(&mut self) {
if let Some(tx) = self.shutdown_tx.take() {
let _ = tx.send(true);
}
if let Some(handle) = self.listener_handle.take() {
let _ = handle.await;
}
if let Some(handle) = self.deadman_handle.take() {
handle.abort();
let _ = handle.await;
}
if let Some(handle) = self.block_announcer_handle.take() {
handle.abort();
let _ = handle.await;
}
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>
2026-04-14 10:24:27 -04:00
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);
2026-03-17 00:03:08 +00:00
info!("Mesh service stopped");
}
/// Get current mesh status.
pub async fn status(&self) -> MeshStatus {
self.state.status.read().await.clone()
}
/// Get a reference to the shared mesh state.
pub fn state(&self) -> &Arc<listener::MeshState> {
&self.state
}
2026-03-17 00:03:08 +00:00
/// Get list of discovered peers.
pub async fn peers(&self) -> Vec<MeshPeer> {
self.state.peers.read().await.values().cloned().collect()
}
/// Get message history.
pub async fn messages(&self, limit: Option<usize>) -> Vec<MeshMessage> {
let messages = self.state.messages.read().await;
let limit = limit.unwrap_or(MAX_MESSAGES_DEFAULT);
// Return in chronological order (oldest first) — take last N items
let len = messages.len();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let skip = len.saturating_sub(limit);
2026-03-17 00:03:08 +00:00
messages.iter().skip(skip).cloned().collect()
}
/// Full in-memory state dump for debugging. Returns peers, all messages,
/// status, shared-secret peer ids (not the secrets), encrypt_relay flag,
/// and stego mode. Intended for development/smoke-test use only — don't
/// call this on a hot path.
pub async fn debug_dump(&self) -> serde_json::Value {
let status = self.state.status.read().await.clone();
let peers: Vec<_> = self.state.peers.read().await.values().cloned().collect();
let messages: Vec<_> = self.state.messages.read().await.iter().cloned().collect();
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let secret_peer_ids: Vec<u32> = self
.state
.shared_secrets
.read()
.await
.keys()
.copied()
.collect();
serde_json::json!({
"status": status,
"peers": peers,
"peer_count": peers.len(),
"messages": messages,
"message_count": messages.len(),
"secret_peer_ids": secret_peer_ids,
"encrypt_relay": self.state.encrypt_relay,
"stego_mode": format!("{:?}", self.state.stego_mode),
})
}
/// Resolve a peer's 6-byte public-key prefix for mesh addressing.
async fn peer_dest_prefix(&self, contact_id: u32) -> Result<[u8; 6]> {
2026-03-17 00:03:08 +00:00
let peers = self.state.peers.read().await;
let peer = peers
.get(&contact_id)
.ok_or_else(|| anyhow::anyhow!("Peer not found"))?;
let pubkey_hex = peer
.pubkey_hex
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Peer has no public key"))?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let pubkey_bytes =
hex::decode(pubkey_hex).map_err(|_| anyhow::anyhow!("Invalid peer public key"))?;
2026-03-17 00:03:08 +00:00
if pubkey_bytes.len() < 6 {
anyhow::bail!("Peer public key too short");
}
let mut dest_prefix = [0u8; 6];
dest_prefix.copy_from_slice(&pubkey_bytes[..6]);
Ok(dest_prefix)
}
2026-03-17 00:03:08 +00:00
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Split an oversized wire payload into MC-framed base64 chunks and send
/// each via the mesh device. Matches the receive-side reassembly in
/// `mesh/listener/decode.rs::handle_chunked_frame` (header `MCIIXXTT`,
/// 20-chunk cap, 152 base64 chars per chunk). The caller must ensure
/// the peer exists and the device is connected.
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
#[allow(dead_code)]
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
async fn send_chunked_payload(&self, contact_id: u32, payload: Vec<u8>) -> Result<()> {
use base64::Engine;
const HEADER_LEN: usize = 8; // MC + msg_id(2) + chunk_idx(2) + total(2)
const MAX_CHUNK_B64: usize = protocol::MAX_MESSAGE_LEN - HEADER_LEN;
const MAX_CHUNKS: u8 = 20;
let b64 = base64::engine::general_purpose::STANDARD.encode(&payload);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let total_chunks = b64.len().div_ceil(MAX_CHUNK_B64) as u8;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
if total_chunks == 0 || total_chunks > MAX_CHUNKS {
anyhow::bail!(
"Payload too large to chunk: {} bytes → {} chunks (max {})",
payload.len(),
total_chunks,
MAX_CHUNKS
);
}
// Pick a 1-byte msg_id. Use the low 8 bits of the unix nanos; not
// cryptographically unique but collisions within a 120s reassembly
// window are astronomically unlikely for normal send rates.
let msg_id: u8 =
(chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0) as u64 & 0xFF) as u8;
let dest_prefix = self.peer_dest_prefix(contact_id).await?;
for chunk_idx in 0..total_chunks {
let start = chunk_idx as usize * MAX_CHUNK_B64;
let end = (start + MAX_CHUNK_B64).min(b64.len());
let chunk = &b64[start..end];
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let frame = format!(
"MC{:02X}{:02X}{:02X}{}",
msg_id, chunk_idx, total_chunks, chunk
);
self.state
.send_cmd(listener::MeshCommand::SendText {
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
dest_pubkey_prefix: dest_prefix,
payload: frame.into_bytes(),
})
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
}
tracing::info!(
contact_id,
msg_id,
chunks = total_chunks,
bytes = payload.len(),
"Sent chunked payload over mesh"
);
Ok(())
}
/// Send raw wire payload bytes to a peer (no Sent-record bookkeeping).
/// Callers are responsible for storing the MeshMessage record afterwards.
///
/// Oversized payloads (>LoRa per-frame budget) are handled by the lower
/// `send_dm_via_channel` layer, which base64-encodes + MC-frame-chunks
/// the bytes into 80-char pieces and reassembles on the receiver. We
/// must NOT chunk here as well — doing so double-chunks and produces
/// bytes the receiver can't decode.
async fn send_raw_payload(&self, contact_id: u32, payload: Vec<u8>) -> Result<()> {
let status = self.state.status.read().await;
if !status.device_connected {
anyhow::bail!("No mesh device connected");
}
drop(status);
2026-03-17 00:03:08 +00:00
let dest_prefix = self.peer_dest_prefix(contact_id).await?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self.state
.send_cmd(listener::MeshCommand::SendText {
2026-03-17 00:03:08 +00:00
dest_pubkey_prefix: dest_prefix,
payload,
})
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
Ok(())
}
/// Send a typed envelope wire payload and record a rich Sent MeshMessage.
/// Used by RPC handlers that transmit invoice/coordinate/alert/etc. so the
/// UI sees a proper rich Sent card instead of garbage wire-byte plaintext.
///
/// `sender_seq` should match the seq encoded into the TypedEnvelope by the
/// caller — the RPC handler allocates it via `next_send_seq` before
/// building the envelope and threads it through here so the local Sent
/// record carries the same MessageKey the receiver will see.
pub async fn send_typed_wire(
&self,
contact_id: u32,
wire: Vec<u8>,
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> Result<MeshMessage> {
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// Federation-synthetic contacts (high bit set) don't exist in the
// meshcore firmware contact table, so LoRa send would fail at
// `peer_dest_prefix`. Any envelope larger than the LoRa frame budget
// also needs the federation path. In both cases we look up the
// peer's onion (by archipelago pubkey first, then by DID) and POST
// over Tor; otherwise the send falls through to LoRa.
let is_federation_synthetic = contact_id & 0x8000_0000 != 0;
let exceeds_lora = wire.len() > protocol::MAX_MESSAGE_LEN;
if is_federation_synthetic || exceeds_lora {
let (peer_pubkey, peer_did) = {
let peers = self.state.peers.read().await;
match peers.get(&contact_id) {
Some(p) => (p.pubkey_hex.clone(), p.did.clone()),
None if is_federation_synthetic => {
anyhow::bail!("Unknown federation peer {}", contact_id);
}
None => (None, None),
}
};
let nodes = crate::federation::load_nodes(&self.data_dir)
.await
.unwrap_or_default();
let onion = peer_pubkey
.as_ref()
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.and_then(|pk| {
nodes
.iter()
.find(|n| &n.pubkey == pk)
.map(|n| n.onion.clone())
})
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
.or_else(|| {
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
peer_did
.as_ref()
.and_then(|d| nodes.iter().find(|n| &n.did == d).map(|n| n.onion.clone()))
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
});
if let Some(onion) = onion {
return self
.send_typed_wire_via_federation(
contact_id,
&onion,
wire,
type_label,
display_text,
typed_payload,
sender_seq,
)
.await;
}
// No federation path — fall through to send_raw_payload, which
// hands the wire to the lower DM-via-channel layer. That layer
// (`send_dm_via_channel` in listener/session.rs) handles both
// single-frame and chunked transmission internally; we must NOT
// pre-chunk here as well or the receiver sees garbage.
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
}
self.send_raw_payload(contact_id, wire).await?;
Ok(self
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.record_sent_typed(
contact_id,
type_label,
display_text,
typed_payload,
sender_seq,
)
.await)
}
/// Send a typed envelope to a peer via the federation (Tor) path rather
/// than LoRa. Used when the envelope exceeds LoRa's per-frame budget —
/// ContentRef is the canonical example, at 400+ bytes with thumb + cap.
/// The peer's onion must already be in federation storage. Records a
/// Sent MeshMessage locally on success so the UI gets the rich card.
///
/// This does NOT use chunking and does NOT go through the mesh radio —
/// it is a straight HTTP POST over Tor to the peer's
/// `/archipelago/mesh-typed` endpoint.
///
/// The POST itself is fire-and-forget: we record the Sent MeshMessage
/// synchronously (so the UI sees the bubble immediately) and spawn the
/// Tor HTTP in the background. Tor circuit setup is 15s per envelope
/// and blocking the RPC on it made `mesh.send` feel laggy — especially
/// over a held Enter key. Delivery failures still surface via the
/// absent read-receipt path: `delivered` stays `false` on the Sent
/// record if the peer never echoes back a receipt.
pub async fn send_typed_wire_via_federation(
&self,
contact_id: u32,
peer_onion: &str,
wire: Vec<u8>,
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> Result<MeshMessage> {
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use ed25519_dalek::Signer;
// Sign the raw wire bytes so the receiver can attribute the envelope
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
// to our pubkey even when it arrives over federation/FIPS/Tor rather
// than the radio. Signature covers the wire only — the receiver
// re-hashes.
let signature = hex::encode(self.signing_key.sign(&wire).to_bytes());
let wire_b64 = BASE64.encode(&wire);
let body = serde_json::json!({
"from_pubkey": self.our_ed_pubkey_hex,
"from_name": self.our_did,
"typed_envelope_b64": wire_b64,
"signature": signature,
});
// Record Sent now so the UI bubble appears immediately.
let msg = self
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.record_sent_typed(
contact_id,
type_label,
display_text,
typed_payload,
sender_seq,
)
.await;
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
// Fire the send in the background. FIPS is preferred when the peer
// is federated and running fips; Tor is the fallback. Failures are
// logged but do not propagate — caller already has the Sent
// MeshMessage and the UI's delivery indicator tracks the receipt.
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
let peer_onion_owned = peer_onion.to_string();
let data_dir_owned = self.data_dir.clone();
tokio::spawn(async move {
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
let fips_npub =
crate::federation::fips_npub_for_onion(&data_dir_owned, &peer_onion_owned).await;
let req = crate::fips::dial::PeerRequest::new(
fips_npub.as_deref(),
&peer_onion_owned,
"/archipelago/mesh-typed",
)
feat(settings): per-service FIPS/Tor transport preference Adds a user-configurable toggle for how each peer-to-peer service reaches federated peers. Three options per service: - Auto (default) — FIPS preferred, Tor fallback (current behavior). - FIPS only — fail rather than fall through to Tor. - Tor only — explicit opt-in to onion anonymity for that service. Services covered (matching the UI rows): - Federation — state sync, invites, peer notifications - Peers — address/DID rotation broadcasts - Peer Files — content catalog download/browse/preview - Messaging — archipelago channel + mesh bridge - Mesh File Sharing — content_ref blob fetches Implementation: - settings::transport — persisted struct + process-wide OnceLock handle (so deep call sites don't need data_dir threaded through signatures). On-disk file: <data_dir>/settings/transport_preferences.json; missing or corrupt → defaults (Auto everywhere). - settings::transport::init() called from main.rs after config load. - fips::dial::PeerRequest gains a .service(kind) builder; send_* checks the preference before choosing a transport. FIPS-only fails loudly when FIPS is unavailable (so users who pick it know when something falls back). - Every FIPS-first migration site tags its PeerRequest with the matching PeerService so the toggle actually applies. - transport.preferences + transport.set-preference RPCs added; wired into the dispatcher. - neode-ui/src/views/settings/TransportPrefsCard.vue — standalone card with a 5-row Auto/FIPS/Tor tri-state. Not wired into Settings.vue — the user places components themselves (see feedback_ui_entry_points). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:44:41 -04:00
.service(crate::settings::transport::PeerService::Messaging)
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
.timeout(std::time::Duration::from_secs(120));
match req.send_json(&body).await {
Ok((resp, transport)) if resp.status().is_success() => {
tracing::debug!(contact_id, transport = %transport, "Federation envelope delivered");
}
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
Ok((resp, transport)) => warn!(
contact_id,
status = %resp.status(),
feat(messaging,dwn,mesh): route peer messaging + DWN sync + blob fetch via FIPS first Migrates the remaining Tor-direct peer call sites to PeerRequest so FIPS is the default when the peer is federated and running the daemon: - node_message::send_to_peer / check_peer_reachable: gain a fips_npub parameter. Error messages updated to reference both transports. - Callers (api/rpc/network.rs, api/rpc/peers.rs, server health loop): look up fips_npub from federation storage by onion and pass it. - mesh::send_typed_wire_via_federation: the spawned background POST for the /archipelago/mesh-typed endpoint now uses PeerRequest with federation-resolved fips_npub. Signature domain unchanged. - api/rpc/mesh/typed_messages.rs fetch_blob_from_peer: blob URL rebuilt as (base_url, path_with_query) so PeerRequest can append the query string after swapping the host. Cap/exp/peer parameters are still signed over the content ref itself, so transport choice is invisible to the signature. - network/dwn_sync.rs sync_with_peers: per-peer fips_npub lookup before sync_single_peer; health/pull/push each dial through PeerRequest, so any DWN peer known to federation gets FIPS. Left Tor-only on purpose: - api/rpc/identity/handlers.rs handle_identity_resolve_peer_onion — resolving TO a DID, no anchor yet. - content.browse / preview calls to non-federated peers fall through to Tor naturally inside PeerRequest (no fips_npub → skip FIPS branch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:36:04 -04:00
transport = %transport,
"Peer rejected federation-routed envelope"
),
Err(e) => warn!(contact_id, "Federation POST failed: {}", e),
}
});
Ok(msg)
}
/// Inject a typed envelope received over federation (Tor) into MeshState
/// as if it had arrived over the mesh radio. Looks up contact_id by
/// matching pubkey_hex against known mesh peers; falls back to a
/// synthetic id derived from the pubkey bytes so the UI can still
/// address the chat (will render as a new "peer" if not in the list).
pub async fn inject_typed_from_federation(
&self,
from_pubkey_hex: &str,
from_name: Option<&str>,
wire: Vec<u8>,
) -> Result<()> {
let envelope = crate::mesh::message_types::TypedEnvelope::from_wire(&wire)?;
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// Federation and mesh have disjoint identity namespaces: a LoRa
// mesh contact carries meshcore's firmware-issued pubkey, not the
// archipelago ed25519 key. So we cannot rely on matching pubkeys
// across transports. Instead, every federation peer has a stable
// synthetic mesh contact_id derived from its archipelago pubkey,
// and is upserted into mesh state the first time we hear from them.
let (federation_did, federation_name) = {
let nodes = crate::federation::load_nodes(&self.data_dir)
.await
.unwrap_or_default();
nodes
.into_iter()
.find(|n| n.pubkey == from_pubkey_hex)
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
.map(|n| (Some(n.did), n.name))
.unwrap_or((None, None))
};
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
// If the federation list knows this sender, use its DID; otherwise
// fall back to `from_name` (the sender's self-reported DID from the
// envelope body, signature-verified upstream in handle_mesh_typed_relay).
let effective_did = federation_did
.or_else(|| from_name.map(|s| s.to_string()))
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.unwrap_or_else(|| {
format!(
"did:unknown:{}",
&from_pubkey_hex[..from_pubkey_hex.len().min(16)]
)
});
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let display_name = federation_name
.clone()
.or_else(|| from_name.map(|s| s.to_string()))
.unwrap_or_else(|| "federation peer".to_string());
let contact_id = upsert_federation_peer(
&self.state,
from_pubkey_hex,
&effective_did,
Some(&display_name),
)
.await;
listener::dispatch::handle_typed_envelope_direct(
&self.state,
contact_id,
&display_name,
envelope,
)
.await;
Ok(())
}
/// Allocate the next outbound seq for a target. Convenience passthrough
/// to MeshState::next_send_seq; used by RPC handlers before encoding a
/// TypedEnvelope so the seq on the wire matches the Sent record.
pub async fn next_send_seq(&self, target: u32) -> u64 {
self.state.next_send_seq(target).await
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Look up a stored MeshMessage by its local `id`. Used by the Forward
/// RPC to pull an existing record's typed payload for re-encoding.
pub async fn find_message_by_id(&self, id: u64) -> Option<MeshMessage> {
let messages = self.state.messages.read().await;
messages.iter().find(|m| m.id == id).cloned()
}
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>
2026-04-14 10:24:27 -04:00
/// 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);
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// 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)".
/// Best-effort: missing target is silently ignored.
pub async fn apply_local_edit(&self, target_seq: u64, new_text: &str, edited_at: u32) {
let mut messages = self.state.messages.write().await;
for m in messages.iter_mut() {
if m.sender_seq == Some(target_seq)
&& matches!(m.direction, crate::mesh::types::MessageDirection::Sent)
{
m.plaintext = new_text.to_string();
let mut obj = match m.typed_payload.take() {
Some(serde_json::Value::Object(o)) => o,
_ => serde_json::Map::new(),
};
obj.insert("edited_at".to_string(), serde_json::json!(edited_at));
obj.insert("text".to_string(), serde_json::json!(new_text));
m.typed_payload = Some(serde_json::Value::Object(obj));
break;
}
}
}
/// Apply a Delete tombstone locally to an own-Sent message.
pub async fn apply_local_delete(&self, target_seq: u64) {
let mut messages = self.state.messages.write().await;
for m in messages.iter_mut() {
if m.sender_seq == Some(target_seq)
&& matches!(m.direction, crate::mesh::types::MessageDirection::Sent)
{
m.plaintext = "🗑 message deleted".to_string();
m.typed_payload = Some(serde_json::json!({ "deleted": true }));
m.message_type = "delete".to_string();
break;
}
}
}
/// Broadcast a typed envelope wire payload on a mesh channel and record a
/// rich Sent MeshMessage. Bytes are sent directly — do NOT utf8_lossy-encode
/// binary envelope bytes before handing them here.
pub async fn send_channel_typed_wire(
&self,
channel: u8,
wire: Vec<u8>,
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> Result<MeshMessage> {
let status = self.state.status.read().await;
if !status.device_connected {
anyhow::bail!("No mesh device connected");
}
drop(status);
if wire.len() > protocol::MAX_MESSAGE_LEN {
anyhow::bail!(
"Message too large for LoRa: {} bytes (max {})",
wire.len(),
protocol::MAX_MESSAGE_LEN
);
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self.state
.send_cmd(listener::MeshCommand::BroadcastChannel {
channel,
payload: wire,
})
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
let chan_contact_id = u32::MAX - (channel as u32);
let chan_name = format!("Channel {}", channel);
let msg_id = self.state.next_id().await;
let msg = MeshMessage {
id: msg_id,
direction: MessageDirection::Sent,
peer_contact_id: chan_contact_id,
peer_name: Some(chan_name),
plaintext: display_text.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
delivered: false,
encrypted: false,
message_type: type_label.to_string(),
typed_payload,
sender_pubkey: Some(self.our_ed_pubkey_hex.clone()),
sender_seq: Some(sender_seq),
};
self.state.store_message(msg.clone()).await;
{
let mut status = self.state.status.write().await;
status.messages_sent += 1;
}
Ok(msg)
}
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
/// Send a text message to a peer. Wraps the text in a typed `Text`
/// envelope (variant 0) with an allocated `sender_seq`, so the resulting
/// MeshMessage carries a stable MessageKey — this is what makes replies
/// and reactions addressable against plain text bubbles.
pub async fn send_message(&self, contact_id: u32, text: &str) -> Result<MeshMessage> {
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
use crate::mesh::message_types::{MeshMessageType, TypedEnvelope};
let seq = self.state.next_send_seq(contact_id).await;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let envelope =
TypedEnvelope::new(MeshMessageType::Text, text.as_bytes().to_vec()).with_seq(seq);
feat(mesh): Phase 1/2b/4/5 primitives — ReadReceipt/Forward/Edit/Delete/Presence/Contacts/ChannelInvite + chunked send + unified inbox RPCs Adds every remaining wire variant and RPC needed to finish the Telegram-quality mesh plan in a single pass: * Variants 15 ReadReceipt, 16 Forward, 17 Edit, 18 Delete, 20 Presence, 21 ChannelInvite; plus MeshMessageType::ContactCard(22) cleanup (was enum-only, now wired through from_u8/label/from_label). * MessageType::from_label() as the inverse of label() — used by the Forward path to re-encode a stored typed body back through its original variant. * RPCs: mesh.send-psbt (variant 3 was previously enum-only), mesh.send-read-receipt, mesh.forward-message, mesh.edit-message, mesh.delete-message, mesh.broadcast-presence, mesh.presence-list, mesh.contacts-list, mesh.contacts-save, mesh.contacts-block, mesh.send-channel-invite, conversations.list, conversations.messages. * MeshState gains presence (pubkey → status+timestamps) and contacts (pubkey → ContactEntry{alias,notes,pinned,blocked}) in-memory stores. * MeshService gains find_message_by_id (Forward lookup), apply_local_edit / apply_local_delete (optimistic local echo), and send_chunked_payload — an MC-framed base64 splitter that fires as a fallback inside send_typed_wire when wire > MAX_MESSAGE_LEN and no federation path is known. Reuses the existing receive-side reassembly in listener/decode.rs. * Receive dispatch arms for PsbtHash, Presence, ChannelInvite, ReadReceipt (rolls forward `delivered` flag on own-Sent ≤ seq for that peer), Forward, Edit, Delete. Edit/Delete guard against cross-peer tampering by matching the target MessageKey pubkey against the sender's advertised pubkey_hex. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:24:05 -04:00
let wire = envelope.to_wire()?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self.send_typed_wire(contact_id, wire, "text", text, None, seq)
.await
2026-03-17 00:03:08 +00:00
}
/// Record a Sent MeshMessage for a typed envelope that has already been
/// transmitted by the caller. Used by the RPC layer after sending
/// invoice/coordinate/alert/etc. so the UI gets a proper rich Sent card
/// instead of a Sent record containing the raw wire bytes as plaintext.
pub async fn record_sent_typed(
&self,
contact_id: u32,
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> MeshMessage {
let msg_id = self.state.next_id().await;
let peer_name = self
.state
.peers
.read()
.await
.get(&contact_id)
.map(|p| p.advert_name.clone());
let msg = MeshMessage {
id: msg_id,
direction: MessageDirection::Sent,
peer_contact_id: contact_id,
peer_name,
plaintext: display_text.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
delivered: false,
encrypted: false,
message_type: type_label.to_string(),
typed_payload,
sender_pubkey: Some(self.our_ed_pubkey_hex.clone()),
sender_seq: Some(sender_seq),
};
self.state.store_message(msg.clone()).await;
{
let mut status = self.state.status.write().await;
status.messages_sent += 1;
}
msg
}
/// Send a message to a mesh channel (broadcast).
/// Routes through the background listener which owns the serial port.
pub async fn send_channel_message(&self, channel: u8, text: &str) -> Result<MeshMessage> {
let status = self.state.status.read().await;
if !status.device_connected {
anyhow::bail!("No mesh device connected");
}
drop(status);
let payload = text.as_bytes().to_vec();
if payload.len() > protocol::MAX_MESSAGE_LEN {
anyhow::bail!(
"Message too large for LoRa: {} bytes (max {})",
payload.len(),
protocol::MAX_MESSAGE_LEN
);
}
// Send through the listener's command channel
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self.state
.send_cmd(listener::MeshCommand::BroadcastChannel { channel, payload })
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
let chan_contact_id = u32::MAX - (channel as u32);
let chan_name = format!("Channel {}", channel);
let msg_id = self.state.next_id().await;
let msg = MeshMessage {
id: msg_id,
direction: MessageDirection::Sent,
peer_contact_id: chan_contact_id,
peer_name: Some(chan_name),
plaintext: text.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
delivered: false,
encrypted: false,
message_type: "text".to_string(),
typed_payload: None,
sender_pubkey: None,
sender_seq: None,
};
self.state.store_message(msg.clone()).await;
{
let mut status = self.state.status.write().await;
status.messages_sent += 1;
}
Ok(msg)
}
2026-03-17 00:03:08 +00:00
/// Broadcast our advertisement over mesh so other nodes can discover us.
/// Sends an immediate advert via the listener's command channel.
pub async fn broadcast_identity(&self) -> Result<()> {
let status = self.state.status.read().await;
if !status.device_connected {
anyhow::bail!("No mesh device connected. Check USB connection.");
}
drop(status);
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
self.state
.send_cmd(listener::MeshCommand::SendAdvert)
2026-03-17 00:03:08 +00:00
.await
.map_err(|_| anyhow::anyhow!("Mesh listener not running"))?;
info!("Mesh self-advert broadcast triggered");
Ok(())
}
/// Update mesh configuration.
pub async fn configure(&mut self, config: MeshConfig) -> Result<()> {
save_config(&self.data_dir, &config).await?;
let was_enabled = self.config.enabled;
self.config = config.clone();
// Update the status to reflect new config
{
let mut status = self.state.status.write().await;
status.enabled = config.enabled;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
status.channel_name = config
.channel_name
.clone()
.unwrap_or_else(|| "archipelago".to_string());
2026-03-17 00:03:08 +00:00
}
// If enabled state changed, start/stop the listener
if config.enabled && !was_enabled {
self.start()?;
} else if !config.enabled && was_enabled {
self.stop().await;
// Clear connected state
let mut status = self.state.status.write().await;
status.device_connected = false;
status.device_path = None;
status.firmware_version = None;
status.self_node_id = None;
status.peer_count = 0;
}
Ok(())
}
/// Get a reference to shared state (for RPC handlers).
pub fn shared_state(&self) -> Arc<MeshState> {
Arc::clone(&self.state)
}
/// Record user activity (resets dead man's switch timer).
pub async fn dead_man_check_in(&self) {
self.dead_man_switch.check_in().await;
}
/// Get our DID.
pub fn our_did(&self) -> &str {
&self.our_did
}
2026-03-17 00:03:08 +00:00
}
const MAX_MESSAGES_DEFAULT: usize = 100;
// ─── Bitcoin RPC helpers for block header announcer ────────────────────
#[derive(serde::Deserialize)]
struct BitcoinRpcResponse<T> {
result: Option<T>,
error: Option<serde_json::Value>,
}
struct BlockHeaderInfo {
hash: String,
prev_hash: String,
timestamp: u32,
}
async fn bitcoin_rpc_getblockcount(client: &reqwest::Client) -> Result<u64> {
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
let body = serde_json::json!({
"jsonrpc": "1.0", "id": "mesh", "method": "getblockcount", "params": []
});
let resp: BitcoinRpcResponse<u64> = tokio::time::timeout(
Duration::from_secs(10),
client
.post(crate::constants::BITCOIN_RPC_URL)
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.send(),
)
.await
.map_err(|_| anyhow::anyhow!("Bitcoin RPC getblockcount timed out after 10s"))?
.map_err(|e| anyhow::anyhow!("Bitcoin RPC send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC parse failed: {}", e))?;
if let Some(err) = resp.error {
anyhow::bail!("Bitcoin RPC: {}", err);
}
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
resp.result
.ok_or_else(|| anyhow::anyhow!("Bitcoin RPC null result"))
}
async fn bitcoin_rpc_getblockheader_by_height(
client: &reqwest::Client,
height: u64,
) -> Result<BlockHeaderInfo> {
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
// First get block hash for this height
let body = serde_json::json!({
"jsonrpc": "1.0", "id": "mesh", "method": "getblockhash", "params": [height]
});
let resp: BitcoinRpcResponse<String> = tokio::time::timeout(
Duration::from_secs(10),
client
.post(crate::constants::BITCOIN_RPC_URL)
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.send(),
)
.await
.map_err(|_| anyhow::anyhow!("Bitcoin RPC getblockhash timed out after 10s"))?
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockhash send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockhash parse failed: {}", e))?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let hash = resp
.result
.ok_or_else(|| anyhow::anyhow!("No block hash"))?;
// Then get full header
let body = serde_json::json!({
"jsonrpc": "1.0", "id": "mesh", "method": "getblockheader", "params": [hash, true]
});
let resp: BitcoinRpcResponse<serde_json::Value> = tokio::time::timeout(
Duration::from_secs(10),
client
.post(crate::constants::BITCOIN_RPC_URL)
.basic_auth(&rpc_user, Some(&rpc_pass))
.json(&body)
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
.send(),
)
.await
.map_err(|_| anyhow::anyhow!("Bitcoin RPC getblockheader timed out after 10s"))?
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockheader send failed: {}", e))?
.json()
.await
.map_err(|e| anyhow::anyhow!("Bitcoin RPC getblockheader parse failed: {}", e))?;
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
let header = resp
.result
.ok_or_else(|| anyhow::anyhow!("No block header"))?;
Ok(BlockHeaderInfo {
hash: header["hash"].as_str().unwrap_or_default().to_string(),
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy with -D warnings, and tests. All three were failing. This commit: - Applies rustfmt across the tree (the bulk of the diff — untouched since the last toolchain bump, so a wide sweep was unavoidable). - Fixes the correctness-level clippy errors: container/bitcoin_simulator.rs wildcard-in-or-pattern container/manifest.rs from_str rename to parse (reserved name) container/podman_client.rs .get(0) -> .first() container/runtime.rs manual += collapse archipelago/src/constants.rs doc-comment → module-doc api/rpc/package/install.rs stray /// comment above a non-item container/docker_packages.rs redundant field init streaming/advertisement.rs missing Metric import in tests tests/orchestration_tests.rs `vec!` in non-Vec contexts mesh/listener/dispatch.rs unused store_plain_message import api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec! - Quiets wide legacy surfaces with crate-level allows in main.rs for stylistic lints (too_many_arguments, type_complexity, doc indent, enum variant prefix, wildcard-in-or, assertions-on-constants, drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens of places with no correctness payoff and have been churning every toolchain bump. - Tags intentional-dead-code helpers: wallet/ and streaming/ modules are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for rollback compatibility, vpn::get_nostr_vpn_status is surface-area for a not-yet-landed RPC. cargo fmt --check, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features now all pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
prev_hash: header["previousblockhash"]
.as_str()
.unwrap_or_default()
.to_string(),
timestamp: header["time"].as_u64().unwrap_or(0) as u32,
})
}
2026-03-17 00:03:08 +00:00
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mesh_config_default() {
let config = MeshConfig::default();
assert!(!config.enabled);
assert_eq!(config.channel_name, Some("archipelago".to_string()));
assert!(config.broadcast_identity);
}
#[test]
fn test_mesh_config_serialization() {
let config = MeshConfig {
enabled: true,
device_path: Some("/dev/ttyUSB0".to_string()),
channel_name: Some("test".to_string()),
broadcast_identity: false,
advert_name: Some("MyNode".to_string()),
..Default::default()
2026-03-17 00:03:08 +00:00
};
let json = serde_json::to_string(&config).unwrap();
let parsed: MeshConfig = serde_json::from_str(&json).unwrap();
assert!(parsed.enabled);
assert_eq!(parsed.device_path, Some("/dev/ttyUSB0".to_string()));
assert_eq!(parsed.advert_name, Some("MyNode".to_string()));
}
#[tokio::test]
async fn test_load_config_default_when_no_file() {
let dir = tempfile::tempdir().unwrap();
let config = load_config(dir.path()).await.unwrap();
assert!(!config.enabled);
}
#[tokio::test]
async fn test_save_and_load_config_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let config = MeshConfig {
enabled: true,
device_path: Some("/dev/ttyUSB0".to_string()),
channel_name: Some("archy".to_string()),
broadcast_identity: true,
advert_name: None,
..Default::default()
2026-03-17 00:03:08 +00:00
};
save_config(dir.path(), &config).await.unwrap();
let loaded = load_config(dir.path()).await.unwrap();
assert!(loaded.enabled);
assert_eq!(loaded.device_path, Some("/dev/ttyUSB0".to_string()));
}
}