use anyhow::{anyhow, Context, Result};
use clap::Subcommand;
use colored::Colorize;
use comfy_table::{presets::UTF8_FULL, Table};
use libp2p::PeerId;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
use firecloud_net::{FireCloudNode, NodeConfig, NodeEvent, MessageResponse};
#[derive(Debug, Subcommand)]
pub enum FriendCommand {
Add {
peer_id: String,
#[arg(short, long)]
name: Option<String>,
},
Accept {
peer_id: String,
},
List {
#[arg(short, long)]
detailed: bool,
},
Remove {
friend: String,
},
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum FriendStatus {
RequestSent,
RequestReceived,
Accepted,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Friend {
pub peer_id: PeerId,
pub name: Option<String>,
pub status: FriendStatus,
pub added_at: chrono::DateTime<chrono::Utc>,
pub last_seen: Option<chrono::DateTime<chrono::Utc>>,
pub messages_exchanged: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FriendList {
pub friends: HashMap<PeerId, Friend>,
}
impl FriendList {
pub fn load(data_dir: &PathBuf) -> Result<Self> {
let friends_path = data_dir.join("friends.json");
if !friends_path.exists() {
return Ok(Self::default());
}
let data = fs::read_to_string(&friends_path)
.context("Failed to read friends.json")?;
let friends: FriendList = serde_json::from_str(&data)
.context("Failed to parse friends.json")?;
Ok(friends)
}
pub fn save(&self, data_dir: &PathBuf) -> Result<()> {
fs::create_dir_all(data_dir)
.context("Failed to create data directory")?;
let friends_path = data_dir.join("friends.json");
let data = serde_json::to_string_pretty(self)
.context("Failed to serialize friends")?;
fs::write(&friends_path, data)
.context("Failed to write friends.json")?;
Ok(())
}
pub fn add_friend(&mut self, peer_id: PeerId, name: Option<String>) -> Result<()> {
if peer_id == self.get_local_peer_id()? {
anyhow::bail!("Cannot add yourself as a friend");
}
if let Some(existing) = self.friends.get(&peer_id) {
match existing.status {
FriendStatus::RequestSent => {
anyhow::bail!("Friend request already sent to this peer");
}
FriendStatus::RequestReceived => {
self.accept_friend(peer_id)?;
return Ok(());
}
FriendStatus::Accepted => {
anyhow::bail!("Already friends with this peer");
}
}
}
let friend = Friend {
peer_id,
name,
status: FriendStatus::RequestSent,
added_at: chrono::Utc::now(),
last_seen: None,
messages_exchanged: 0,
};
self.friends.insert(peer_id, friend);
Ok(())
}
pub fn accept_friend(&mut self, peer_id: PeerId) -> Result<()> {
let friend = self.friends.get_mut(&peer_id)
.context("Friend not found in list")?;
match friend.status {
FriendStatus::RequestReceived | FriendStatus::RequestSent => {
friend.status = FriendStatus::Accepted;
Ok(())
}
FriendStatus::Accepted => {
anyhow::bail!("Already accepted this friend");
}
}
}
pub fn remove_friend(&mut self, peer_id: &PeerId) -> Result<()> {
self.friends.remove(peer_id)
.context("Friend not found in list")?;
Ok(())
}
pub fn is_friend(&self, peer_id: &PeerId) -> bool {
self.friends.get(peer_id)
.map(|f| f.status == FriendStatus::Accepted)
.unwrap_or(false)
}
pub fn find_friend(&self, identifier: &str) -> Option<&Friend> {
if let Ok(peer_id) = PeerId::from_str(identifier) {
if let Some(friend) = self.friends.get(&peer_id) {
return Some(friend);
}
}
self.friends.values()
.find(|f| f.name.as_ref().map(|n| n == identifier).unwrap_or(false))
}
pub fn get_accepted_friends(&self) -> Vec<&Friend> {
self.friends.values()
.filter(|f| f.status == FriendStatus::Accepted)
.collect()
}
pub fn get_pending_received(&self) -> Vec<&Friend> {
self.friends.values()
.filter(|f| f.status == FriendStatus::RequestReceived)
.collect()
}
pub fn get_pending_sent(&self) -> Vec<&Friend> {
self.friends.values()
.filter(|f| f.status == FriendStatus::RequestSent)
.collect()
}
pub fn update_last_seen(&mut self, peer_id: &PeerId) {
if let Some(friend) = self.friends.get_mut(peer_id) {
friend.last_seen = Some(chrono::Utc::now());
}
}
pub fn increment_messages(&mut self, peer_id: &PeerId) {
if let Some(friend) = self.friends.get_mut(peer_id) {
friend.messages_exchanged += 1;
}
}
fn get_local_peer_id(&self) -> Result<PeerId> {
Err(anyhow::anyhow!("Local peer ID not available"))
}
}
async fn create_friend_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_friend_command(cmd: FriendCommand, data_dir: PathBuf) -> Result<()> {
let mut friends = FriendList::load(&data_dir)?;
match cmd {
FriendCommand::Add { peer_id, name } => {
let peer_id = PeerId::from_str(&peer_id)
.context("Invalid peer ID format")?;
friends.add_friend(peer_id, name.clone())?;
friends.save(&data_dir)?;
let peer_id_string = peer_id.to_string();
let display_name = name.as_ref().map(|s| s.as_str()).unwrap_or(&peer_id_string);
println!("\n{}", "📡 Sending friend request over network...".cyan());
match create_friend_node().await {
Ok(mut node) => {
let _request_id = node.send_friend_request(&peer_id, name.clone());
println!("{}", " Network request sent!".green());
let timeout = tokio::time::timeout(Duration::from_secs(3), async {
while let Some(event) = node.poll_event().await {
if let NodeEvent::MessageResponse { response, .. } = event {
if let MessageResponse::FriendRequestReceived { mutual } = response {
if mutual {
println!("{}", "🎉 Mutual friend request detected - auto-accepted!".green().bold());
friends.accept_friend(peer_id)?;
friends.save(&data_dir)?;
}
return Ok::<(), anyhow::Error>(());
}
}
}
Ok(())
});
let _ = timeout.await;
println!("\n{}", "✅ Friend request sent!".green().bold());
println!(" Peer ID: {}", peer_id.to_string().cyan());
println!(" Name: {}", display_name.yellow());
println!("\n{}", "Waiting for them to accept your request...".dimmed());
}
Err(e) => {
println!("{}", format!("⚠️ Network error: {}", e).yellow());
println!("{}", " Request saved locally. Will retry when network is available.".dimmed());
}
}
}
FriendCommand::Accept { peer_id } => {
let peer_id = PeerId::from_str(&peer_id)
.context("Invalid peer ID format")?;
friends.accept_friend(peer_id)?;
friends.save(&data_dir)?;
let friend = friends.friends.get(&peer_id).unwrap();
let display_name = friend.name.clone().unwrap_or_else(|| peer_id.to_string());
println!("\n{}", "📡 Sending friend acceptance over network...".cyan());
match create_friend_node().await {
Ok(mut node) => {
let _request_id = node.send_friend_accept(&peer_id);
println!("{}", " Network confirmation sent!".green());
tokio::time::sleep(Duration::from_secs(2)).await;
}
Err(e) => {
println!("{}", format!("⚠️ Network error: {}", e).yellow());
println!("{}", " Acceptance saved locally. Will retry when network is available.".dimmed());
}
}
println!("\n{}", "✅ Friend request accepted!".green().bold());
println!(" You are now friends with: {}", display_name.yellow());
println!(" Peer ID: {}", peer_id.to_string().cyan());
println!("\n{}", "You can now send private messages and files!".dimmed());
}
FriendCommand::List { detailed } => {
let accepted = friends.get_accepted_friends();
if accepted.is_empty() {
println!("\n{}", "No friends yet.".yellow());
println!("{}", "Add friends with: firecloud friend add <peer-id>".dimmed());
return Ok(());
}
println!("\n{} ({} total)", "Friends".green().bold(), accepted.len());
if detailed {
for friend in accepted {
let display_name = friend.name.clone()
.unwrap_or_else(|| format!("{:.8}...", friend.peer_id.to_string()));
let last_seen = friend.last_seen
.map(|t| format!("{}", humantime::format_duration(
chrono::Utc::now().signed_duration_since(t).to_std().unwrap_or_default()
)))
.unwrap_or_else(|| "Never".to_string());
println!("\n {} {}", "•".cyan(), display_name.yellow().bold());
println!(" Peer ID: {}", friend.peer_id.to_string().dimmed());
println!(" Added: {}", friend.added_at.format("%Y-%m-%d %H:%M").to_string().dimmed());
println!(" Last seen: {}", last_seen.dimmed());
println!(" Messages: {}", friend.messages_exchanged.to_string().dimmed());
}
} else {
let mut table = Table::new();
table.load_preset(UTF8_FULL);
table.set_header(vec!["Name", "Peer ID", "Last Seen", "Messages"]);
for friend in accepted {
let display_name = friend.name.clone()
.unwrap_or_else(|| format!("{:.8}...", friend.peer_id.to_string()));
let peer_id_short = format!("{:.12}...", friend.peer_id.to_string());
let last_seen = friend.last_seen
.map(|t| {
let duration = chrono::Utc::now().signed_duration_since(t);
if duration.num_hours() < 1 {
format!("{}m ago", duration.num_minutes())
} else if duration.num_days() < 1 {
format!("{}h ago", duration.num_hours())
} else {
format!("{}d ago", duration.num_days())
}
})
.unwrap_or_else(|| "Never".to_string());
table.add_row(vec![
display_name,
peer_id_short,
last_seen,
friend.messages_exchanged.to_string(),
]);
}
println!("{}", table);
}
}
FriendCommand::Remove { friend: identifier } => {
let friend = friends.find_friend(&identifier)
.context("Friend not found")?;
let peer_id = friend.peer_id;
let display_name = friend.name.clone()
.unwrap_or_else(|| peer_id.to_string());
friends.remove_friend(&peer_id)?;
friends.save(&data_dir)?;
println!("\n{}", "✅ Friend removed".green().bold());
println!(" {}", display_name.yellow());
}
FriendCommand::Pending => {
let received = friends.get_pending_received();
let sent = friends.get_pending_sent();
if received.is_empty() && sent.is_empty() {
println!("\n{}", "No pending friend requests.".yellow());
return Ok(());
}
if !received.is_empty() {
println!("\n{} ({})", "Received Requests".green().bold(), received.len());
for friend in received {
let display_name = friend.name.clone()
.unwrap_or_else(|| format!("{:.8}...", friend.peer_id.to_string()));
println!(" {} {}", "•".cyan(), display_name.yellow());
println!(" Peer ID: {}", friend.peer_id.to_string().dimmed());
println!(" Accept with: {}",
format!("firecloud friend accept {}", friend.peer_id).cyan());
}
}
if !sent.is_empty() {
println!("\n{} ({})", "Sent Requests".yellow().bold(), sent.len());
for friend in sent {
let display_name = friend.name.clone()
.unwrap_or_else(|| format!("{:.8}...", friend.peer_id.to_string()));
println!(" {} {}", "•".cyan(), display_name.yellow());
println!(" Peer ID: {}", friend.peer_id.to_string().dimmed());
println!(" {}", "Waiting for acceptance...".dimmed());
}
}
}
}
Ok(())
}