use anyhow::{anyhow, Context, Result};
use clap::Subcommand;
use colored::Colorize;
use libp2p::PeerId;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use chrono::{DateTime, Utc};
use rand::Rng;
use super::friend::FriendList;
use firecloud_net::{FireCloudNode, NodeConfig};
const MESSAGE_SIZE: usize = 1024;
const MIN_DELAY_MS: u64 = 100;
const MAX_DELAY_MS: u64 = 500;
#[derive(Debug, Subcommand)]
pub enum MessageCommand {
Send {
friend: String,
message: String,
},
Inbox {
#[arg(short, long)]
from: Option<String>,
#[arg(short, long, default_value = "20")]
limit: usize,
},
Chat {
friend: String,
#[arg(short, long, default_value = "50")]
limit: usize,
},
Clear {
friend: Option<String>,
#[arg(short, long)]
yes: bool,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: String,
pub from: PeerId,
pub to: PeerId,
pub content: Vec<u8>,
pub timestamp: DateTime<Utc>,
pub delivered: bool,
pub read: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecryptedMessage {
pub id: String,
pub from: PeerId,
pub to: PeerId,
pub content: String,
pub timestamp: DateTime<Utc>,
pub delivered: bool,
pub read: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MessageStore {
pub messages: HashMap<String, Message>,
pub by_peer: HashMap<PeerId, Vec<String>>, }
impl MessageStore {
pub fn load(data_dir: &PathBuf) -> Result<Self> {
let messages_path = data_dir.join("messages.json");
if !messages_path.exists() {
return Ok(Self::default());
}
let data = fs::read_to_string(&messages_path)
.context("Failed to read messages.json")?;
let store: MessageStore = serde_json::from_str(&data)
.context("Failed to parse messages.json")?;
Ok(store)
}
pub fn save(&self, data_dir: &PathBuf) -> Result<()> {
fs::create_dir_all(data_dir)
.context("Failed to create data directory")?;
let messages_path = data_dir.join("messages.json");
let data = serde_json::to_string_pretty(self)
.context("Failed to serialize messages")?;
fs::write(&messages_path, data)
.context("Failed to write messages.json")?;
Ok(())
}
pub fn add_message(&mut self, message: Message) {
let msg_id = message.id.clone();
let peer_id = if message.from == self.get_local_peer_id() {
message.to
} else {
message.from
};
self.messages.insert(msg_id.clone(), message);
self.by_peer.entry(peer_id)
.or_insert_with(Vec::new)
.push(msg_id);
}
pub fn get_conversation(&self, peer_id: &PeerId, limit: usize) -> Vec<&Message> {
if let Some(msg_ids) = self.by_peer.get(peer_id) {
msg_ids.iter()
.rev()
.take(limit)
.filter_map(|id| self.messages.get(id))
.collect()
} else {
Vec::new()
}
}
pub fn get_inbox(&self, limit: usize) -> Vec<&Message> {
let local_peer_id = self.get_local_peer_id();
let mut messages: Vec<&Message> = self.messages.values()
.filter(|m| m.to == local_peer_id)
.collect();
messages.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
messages.into_iter().take(limit).collect()
}
pub fn mark_read(&mut self, message_id: &str) -> Result<()> {
let message = self.messages.get_mut(message_id)
.context("Message not found")?;
message.read = true;
Ok(())
}
pub fn mark_delivered(&mut self, message_id: &str) {
if let Some(message) = self.messages.get_mut(message_id) {
message.delivered = true;
}
}
pub fn clear_conversation(&mut self, peer_id: &PeerId) -> Result<usize> {
if let Some(msg_ids) = self.by_peer.remove(peer_id) {
let count = msg_ids.len();
for msg_id in msg_ids {
self.messages.remove(&msg_id);
}
Ok(count)
} else {
Ok(0)
}
}
fn get_local_peer_id(&self) -> PeerId {
PeerId::random()
}
}
pub fn encrypt_message(content: &str, _recipient: &PeerId) -> Result<Vec<u8>> {
let bytes = content.as_bytes();
if bytes.len() > MESSAGE_SIZE - 16 { anyhow::bail!("Message too long (max {} chars)", MESSAGE_SIZE - 16);
}
let mut padded = Vec::with_capacity(MESSAGE_SIZE);
padded.extend_from_slice(bytes);
let original_len = bytes.len() as u32;
let padding_len = MESSAGE_SIZE - bytes.len() - 4;
let mut rng = rand::thread_rng();
use rand::Rng;
for _ in 0..padding_len {
padded.push(rng.gen());
}
padded.extend_from_slice(&original_len.to_le_bytes());
Ok(padded)
}
pub fn decrypt_message(encrypted: &[u8], _sender: &PeerId) -> Result<String> {
if encrypted.len() != MESSAGE_SIZE {
anyhow::bail!("Invalid message size");
}
let len_bytes: [u8; 4] = encrypted[MESSAGE_SIZE - 4..]
.try_into()
.context("Failed to extract length")?;
let original_len = u32::from_le_bytes(len_bytes) as usize;
if original_len > MESSAGE_SIZE - 4 {
anyhow::bail!("Invalid message length");
}
let content_bytes = &encrypted[..original_len];
let content = String::from_utf8(content_bytes.to_vec())
.context("Invalid UTF-8 in message")?;
Ok(content)
}
async fn create_message_node() -> Result<FireCloudNode> {
let config = NodeConfig {
port: 0, bootstrap_peers: vec![],
enable_mdns: true,
bootstrap_relays: vec![],
};
FireCloudNode::new(config).await
.context("Failed to create network node")
}
pub async fn handle_message_command(
cmd: MessageCommand,
data_dir: PathBuf,
) -> Result<()> {
let friends = FriendList::load(&data_dir)?;
let mut messages = MessageStore::load(&data_dir)?;
match cmd {
MessageCommand::Send { friend, message } => {
let friend_data = friends.find_friend(&friend)
.context("Friend not found. Add them first with: firecloud friend add <peer-id>")?;
if !friends.is_friend(&friend_data.peer_id) {
anyhow::bail!(
"Not yet friends with {}. Wait for them to accept your request.",
friend_data.name.as_ref().unwrap_or(&friend)
);
}
let encrypted = encrypt_message(&message, &friend_data.peer_id)?;
let delay = rand::thread_rng().gen_range(MIN_DELAY_MS..=MAX_DELAY_MS);
tokio::time::sleep(Duration::from_millis(delay)).await;
let message_id = uuid::Uuid::new_v4().to_string();
let timestamp = Utc::now().timestamp();
println!("\n{}", "📡 Sending encrypted message over network...".cyan());
match create_message_node().await {
Ok(mut node) => {
let _request_id = node.send_direct_message(
&friend_data.peer_id,
encrypted.clone(),
message_id.clone(),
timestamp,
);
println!("{}", " Message sent! (1024 bytes - padded for privacy)".green());
tokio::time::sleep(Duration::from_secs(2)).await;
}
Err(e) => {
println!("{}", format!("⚠️ Network error: {}", e).yellow());
println!("{}", " Message saved locally. Will retry when network is available.".dimmed());
}
}
let msg = Message {
id: message_id,
from: messages.get_local_peer_id(),
to: friend_data.peer_id,
content: encrypted,
timestamp: DateTime::from_timestamp(timestamp, 0).unwrap_or(Utc::now()),
delivered: false, read: false,
};
messages.add_message(msg);
messages.save(&data_dir)?;
let peer_id_string = friend_data.peer_id.to_string();
let display_name = friend_data.name.as_ref()
.unwrap_or(&peer_id_string);
println!("\n{}", "✅ Message sent!".green().bold());
println!(" To: {}", display_name.yellow());
println!(" Delay: {}ms (timing obfuscation)", delay.to_string().dimmed());
println!(" Size: {} bytes (padded for metadata privacy)", MESSAGE_SIZE.to_string().cyan());
println!("\n{}", "Message encrypted and metadata hidden with padding + timing delays.".dimmed());
}
MessageCommand::Inbox { from, limit } => {
let inbox = if let Some(friend_name) = from {
let friend_data = friends.find_friend(&friend_name)
.context("Friend not found")?;
messages.get_conversation(&friend_data.peer_id, limit)
} else {
messages.get_inbox(limit)
};
if inbox.is_empty() {
println!("\n{}", "No messages.".yellow());
return Ok(());
}
println!("\n{} ({} messages)", "Inbox".green().bold(), inbox.len());
for msg in inbox.iter().rev() {
let sender = friends.friends.get(&msg.from);
let peer_id_str = msg.from.to_string();
let sender_name = sender
.and_then(|f| f.name.as_ref())
.map(|n| n.as_str())
.unwrap_or_else(|| {
&peer_id_str[..8.min(peer_id_str.len())]
});
let content = decrypt_message(&msg.content, &msg.from)
.unwrap_or_else(|_| "[Encrypted]".to_string());
let time_ago = humantime::format_duration(
Utc::now().signed_duration_since(msg.timestamp)
.to_std()
.unwrap_or_default()
);
let read_marker = if msg.read { "" } else { "●" };
println!("\n {} {} {} {}",
read_marker.cyan(),
sender_name.yellow().bold(),
format!("({})", time_ago).dimmed(),
if msg.delivered { "✓".green() } else { "⏳".yellow() }
);
println!(" {}", content);
}
}
MessageCommand::Chat { friend, limit } => {
let friend_data = friends.find_friend(&friend)
.context("Friend not found")?;
let conversation = messages.get_conversation(&friend_data.peer_id, limit);
if conversation.is_empty() {
println!("\n{}", "No messages yet.".yellow());
println!("Send a message with: {}",
format!("firecloud msg send '{}' 'Hello!'", friend).cyan());
return Ok(());
}
let peer_id_string = friend_data.peer_id.to_string();
let display_name = friend_data.name.as_ref()
.unwrap_or(&peer_id_string);
println!("\n{} {}", "Chat with".green().bold(), display_name.yellow().bold());
println!("{}", "─".repeat(50).dimmed());
let local_peer_id = messages.get_local_peer_id();
for msg in conversation.iter().rev() {
let is_me = msg.from == local_peer_id;
let content = decrypt_message(&msg.content, &msg.from)
.unwrap_or_else(|_| "[Encrypted]".to_string());
let time = msg.timestamp.format("%H:%M").to_string();
if is_me {
println!(" {} {} {}",
time.dimmed(),
"You:".cyan().bold(),
content
);
} else {
println!(" {} {} {}",
time.dimmed(),
format!("{}:", display_name).yellow().bold(),
content
);
}
}
}
MessageCommand::Clear { friend, yes } => {
if !yes {
println!("{}", "This will delete message history. Use --yes to confirm.".yellow());
return Ok(());
}
let count = if let Some(friend_name) = friend {
let friend_data = friends.find_friend(&friend_name)
.context("Friend not found")?;
messages.clear_conversation(&friend_data.peer_id)?
} else {
let total = messages.messages.len();
messages.messages.clear();
messages.by_peer.clear();
total
};
messages.save(&data_dir)?;
println!("\n{}", format!("✅ Cleared {} messages", count).green().bold());
}
}
Ok(())
}