diff --git a/src-tauri/src/commands/chat.rs b/src-tauri/src/commands/chat.rs index d5a88cd..bc15809 100644 --- a/src-tauri/src/commands/chat.rs +++ b/src-tauri/src/commands/chat.rs @@ -12,8 +12,11 @@ use crate::protocol::messages::{ use crate::verification; use crate::AppState; +use super::ipc_log; + #[tauri::command] pub async fn start_node(app: tauri::AppHandle, state: State<'_, AppState>) -> Result<(), String> { + ipc_log!("start_node", { let identity = state.identity.lock().await; let id = identity .as_ref() @@ -205,10 +208,12 @@ pub async fn start_node(app: tauri::AppHandle, state: State<'_, AppState>) -> Re } Ok(()) + }) } #[tauri::command] pub async fn stop_node(state: State<'_, AppState>) -> Result<(), String> { + ipc_log!("stop_node", { let mut node_handle = state.node_handle.lock().await; if let Some(handle) = node_handle.take() { @@ -224,6 +229,7 @@ pub async fn stop_node(state: State<'_, AppState>) -> Result<(), String> { } Ok(()) + }) } #[tauri::command] @@ -232,6 +238,7 @@ pub async fn send_message( channel_id: String, content: String, ) -> Result { + ipc_log!("send_message", { let identity = state.identity.lock().await; let id = identity.as_ref().ok_or("no identity loaded")?; @@ -271,6 +278,7 @@ pub async fn send_message( } Ok(msg) + }) } #[tauri::command] @@ -280,13 +288,16 @@ pub async fn get_messages( before: Option, limit: Option, ) -> Result, String> { + ipc_log!("get_messages", { let engine = state.crdt_engine.lock().await; let community_id = find_community_for_channel(&engine, &channel_id)?; engine.get_messages(&community_id, &channel_id, before, limit.unwrap_or(50)) + }) } #[tauri::command] pub async fn send_typing(state: State<'_, AppState>, channel_id: String) -> Result<(), String> { + ipc_log!("send_typing", { let identity = state.identity.lock().await; let id = identity.as_ref().ok_or("no identity loaded")?; @@ -318,11 +329,13 @@ pub async fn send_typing(state: State<'_, AppState>, channel_id: String) -> Resu } Ok(()) + }) } // broadcast current user status to all joined communities #[tauri::command] pub async fn broadcast_presence(state: State<'_, AppState>, status: String) -> Result<(), String> { + ipc_log!("broadcast_presence", { let peer_status = match status.as_str() { "online" => PeerStatus::Online, "idle" => PeerStatus::Idle, @@ -343,6 +356,7 @@ pub async fn broadcast_presence(state: State<'_, AppState>, status: String) -> R } Ok(()) + }) } // find which community a channel belongs to by checking all loaded documents @@ -367,6 +381,7 @@ fn find_community_for_channel( // between a general internet outage and the relay being unreachable #[tauri::command] pub async fn check_internet_connectivity() -> Result { + ipc_log!("check_internet_connectivity", { let hosts = vec![ ("www.apple.com", 80), ("www.google.com", 80), @@ -386,4 +401,5 @@ pub async fn check_internet_connectivity() -> Result { let results = futures::future::join_all(futures).await; Ok(results.iter().any(|r| matches!(r, Ok(Ok(_))))) + }) } diff --git a/src-tauri/src/commands/community.rs b/src-tauri/src/commands/community.rs index 3225bc1..d72d9c3 100644 --- a/src-tauri/src/commands/community.rs +++ b/src-tauri/src/commands/community.rs @@ -3,6 +3,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use sha2::{Digest, Sha256}; use tauri::State; +use super::ipc_log; use crate::crdt::sync::{DocumentSnapshot, SyncMessage}; use crate::node::gossip; use crate::node::NodeCommand; @@ -100,74 +101,76 @@ pub async fn create_community( name: String, description: String, ) -> Result { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; + ipc_log!("create_community", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; - // generate a deterministic community id from name + creator + timestamp - let mut hasher = Sha256::new(); - hasher.update(name.as_bytes()); - hasher.update(id.peer_id.to_bytes()); - hasher.update(now.to_le_bytes()); - let hash = hasher.finalize(); - let community_id = format!("com_{}", &hex::encode(hash)[..16]); + // generate a deterministic community id from name + creator + timestamp + let mut hasher = Sha256::new(); + hasher.update(name.as_bytes()); + hasher.update(id.peer_id.to_bytes()); + hasher.update(now.to_le_bytes()); + let hash = hasher.finalize(); + let community_id = format!("com_{}", &hex::encode(hash)[..16]); - let peer_id_str = id.peer_id.to_string(); - drop(identity); + let peer_id_str = id.peer_id.to_string(); + drop(identity); - let mut engine = state.crdt_engine.lock().await; - engine.create_community(&community_id, &name, &description, &peer_id_str)?; + let mut engine = state.crdt_engine.lock().await; + engine.create_community(&community_id, &name, &description, &peer_id_str)?; - let meta = engine.get_community_meta(&community_id)?; + let meta = engine.get_community_meta(&community_id)?; - // save metadata cache - let _ = state.storage.save_community_meta(&meta); - drop(engine); + // save metadata cache + let _ = state.storage.save_community_meta(&meta); + drop(engine); - // subscribe to community topics on the p2p node - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let presence_topic = gossip::topic_for_presence(&community_id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { - topic: presence_topic, - }) - .await; + // subscribe to community topics on the p2p node + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let presence_topic = gossip::topic_for_presence(&community_id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { + topic: presence_topic, + }) + .await; - // subscribe to the default general channel - let engine = state.crdt_engine.lock().await; - if let Ok(channels) = engine.get_channels(&community_id) { - for channel in &channels { - let msg_topic = gossip::topic_for_messages(&community_id, &channel.id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { topic: msg_topic }) - .await; + // subscribe to the default general channel + let engine = state.crdt_engine.lock().await; + if let Ok(channels) = engine.get_channels(&community_id) { + for channel in &channels { + let msg_topic = gossip::topic_for_messages(&community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { topic: msg_topic }) + .await; - let typing_topic = gossip::topic_for_typing(&community_id, &channel.id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { - topic: typing_topic, - }) - .await; + let typing_topic = gossip::topic_for_typing(&community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { + topic: typing_topic, + }) + .await; + } } + + // register on rendezvous so peers joining via invite can discover us + let namespace = format!("dusk/community/{}", community_id); + let _ = handle + .command_tx + .send(NodeCommand::RegisterRendezvous { namespace }) + .await; } - // register on rendezvous so peers joining via invite can discover us - let namespace = format!("dusk/community/{}", community_id); - let _ = handle - .command_tx - .send(NodeCommand::RegisterRendezvous { namespace }) - .await; - } - - Ok(meta) + Ok(meta) + }) } #[tauri::command] @@ -175,103 +178,105 @@ pub async fn join_community( state: State<'_, AppState>, invite_code: String, ) -> Result { - let invite = crate::protocol::community::InviteCode::decode(&invite_code)?; + ipc_log!("join_community", { + let invite = crate::protocol::community::InviteCode::decode(&invite_code)?; - let local_peer_id = { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - id.peer_id.to_string() - }; + let local_peer_id = { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + id.peer_id.to_string() + }; - // create a placeholder document that will be backfilled via crdt sync - // once we connect to existing community members through the relay - let mut engine = state.crdt_engine.lock().await; - let had_existing_doc = engine.has_community(&invite.community_id); - if !had_existing_doc { - engine.create_placeholder_community(&invite.community_id, &invite.community_name, "")?; - } + // create a placeholder document that will be backfilled via crdt sync + // once we connect to existing community members through the relay + let mut engine = state.crdt_engine.lock().await; + let had_existing_doc = engine.has_community(&invite.community_id); + if !had_existing_doc { + engine.create_placeholder_community(&invite.community_id, &invite.community_name, "")?; + } - // joining via invite must never keep elevated local roles from stale local docs - if had_existing_doc { - if let Ok(members) = engine.get_members(&invite.community_id) { - let local_has_elevated_role = members.iter().any(|member| { - member.peer_id == local_peer_id - && member - .roles - .iter() - .any(|role| role == "owner" || role == "admin") - }); + // joining via invite must never keep elevated local roles from stale local docs + if had_existing_doc { + if let Ok(members) = engine.get_members(&invite.community_id) { + let local_has_elevated_role = members.iter().any(|member| { + member.peer_id == local_peer_id + && member + .roles + .iter() + .any(|role| role == "owner" || role == "admin") + }); - if local_has_elevated_role { - let roles = vec!["member".to_string()]; - let _ = engine.set_member_role(&invite.community_id, &local_peer_id, &roles); + if local_has_elevated_role { + let roles = vec!["member".to_string()]; + let _ = engine.set_member_role(&invite.community_id, &local_peer_id, &roles); + } } } - } - let meta = engine.get_community_meta(&invite.community_id)?; - let _ = state.storage.save_community_meta(&meta); + let meta = engine.get_community_meta(&invite.community_id)?; + let _ = state.storage.save_community_meta(&meta); - // subscribe to gossipsub topics so we receive messages - let channels = engine - .get_channels(&invite.community_id) - .unwrap_or_default(); - drop(engine); + // subscribe to gossipsub topics so we receive messages + let channels = engine + .get_channels(&invite.community_id) + .unwrap_or_default(); + drop(engine); - // mark this community for one-time role hardening on first sync merge - { - let mut guard = state.pending_join_role_guard.lock().await; - guard.insert(invite.community_id.clone()); - } + // mark this community for one-time role hardening on first sync merge + { + let mut guard = state.pending_join_role_guard.lock().await; + guard.insert(invite.community_id.clone()); + } - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - // subscribe to the community presence topic - let presence_topic = gossip::topic_for_presence(&invite.community_id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { - topic: presence_topic, - }) - .await; - - // subscribe to all channel topics - for channel in &channels { - let msg_topic = gossip::topic_for_messages(&invite.community_id, &channel.id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { topic: msg_topic }) - .await; - - let typing_topic = gossip::topic_for_typing(&invite.community_id, &channel.id); + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + // subscribe to the community presence topic + let presence_topic = gossip::topic_for_presence(&invite.community_id); let _ = handle .command_tx .send(NodeCommand::Subscribe { - topic: typing_topic, + topic: presence_topic, }) .await; + + // subscribe to all channel topics + for channel in &channels { + let msg_topic = gossip::topic_for_messages(&invite.community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { topic: msg_topic }) + .await; + + let typing_topic = gossip::topic_for_typing(&invite.community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { + topic: typing_topic, + }) + .await; + } + + // register on rendezvous so existing members can find us + let namespace = format!("dusk/community/{}", invite.community_id); + let _ = handle + .command_tx + .send(NodeCommand::RegisterRendezvous { + namespace: namespace.clone(), + }) + .await; + + // discover existing members through rendezvous + let _ = handle + .command_tx + .send(NodeCommand::DiscoverRendezvous { namespace }) + .await; } - // register on rendezvous so existing members can find us - let namespace = format!("dusk/community/{}", invite.community_id); - let _ = handle - .command_tx - .send(NodeCommand::RegisterRendezvous { - namespace: namespace.clone(), - }) - .await; + // request a snapshot now so joins work even when peers were already connected + request_sync(&state).await; - // discover existing members through rendezvous - let _ = handle - .command_tx - .send(NodeCommand::DiscoverRendezvous { namespace }) - .await; - } - - // request a snapshot now so joins work even when peers were already connected - request_sync(&state).await; - - Ok(meta) + Ok(meta) + }) } #[tauri::command] @@ -279,90 +284,94 @@ pub async fn leave_community( state: State<'_, AppState>, community_id: String, ) -> Result<(), String> { - let local_peer_id = { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - id.peer_id.to_string() - }; + ipc_log!("leave_community", { + let local_peer_id = { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + id.peer_id.to_string() + }; - // remove local user from the shared member list before leaving - let mut removed_self = false; - let channels = { - let mut engine = state.crdt_engine.lock().await; - let channels = engine.get_channels(&community_id).unwrap_or_default(); + // remove local user from the shared member list before leaving + let mut removed_self = false; + let channels = { + let mut engine = state.crdt_engine.lock().await; + let channels = engine.get_channels(&community_id).unwrap_or_default(); - if let Ok(members) = engine.get_members(&community_id) { - if members.iter().any(|member| member.peer_id == local_peer_id) { - if engine.remove_member(&community_id, &local_peer_id).is_ok() { - removed_self = true; + if let Ok(members) = engine.get_members(&community_id) { + if members.iter().any(|member| member.peer_id == local_peer_id) { + if engine.remove_member(&community_id, &local_peer_id).is_ok() { + removed_self = true; + } } } + + channels + }; + + if removed_self { + broadcast_sync(&state, &community_id).await; } - channels - }; + // unsubscribe from all community topics and stop advertising this namespace + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + for channel in &channels { + let msg_topic = gossip::topic_for_messages(&community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Unsubscribe { topic: msg_topic }) + .await; - if removed_self { - broadcast_sync(&state, &community_id).await; - } + let typing_topic = gossip::topic_for_typing(&community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Unsubscribe { + topic: typing_topic, + }) + .await; + } - // unsubscribe from all community topics and stop advertising this namespace - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - for channel in &channels { - let msg_topic = gossip::topic_for_messages(&community_id, &channel.id); - let _ = handle - .command_tx - .send(NodeCommand::Unsubscribe { topic: msg_topic }) - .await; - - let typing_topic = gossip::topic_for_typing(&community_id, &channel.id); + let presence_topic = gossip::topic_for_presence(&community_id); let _ = handle .command_tx .send(NodeCommand::Unsubscribe { - topic: typing_topic, + topic: presence_topic, }) .await; + + let namespace = format!("dusk/community/{}", community_id); + let _ = handle + .command_tx + .send(NodeCommand::UnregisterRendezvous { namespace }) + .await; } - let presence_topic = gossip::topic_for_presence(&community_id); - let _ = handle - .command_tx - .send(NodeCommand::Unsubscribe { - topic: presence_topic, - }) - .await; + // remove local cached community state so leave persists across restarts + let mut engine = state.crdt_engine.lock().await; + engine.remove_community(&community_id)?; + drop(engine); - let namespace = format!("dusk/community/{}", community_id); - let _ = handle - .command_tx - .send(NodeCommand::UnregisterRendezvous { namespace }) - .await; - } + let mut guard = state.pending_join_role_guard.lock().await; + guard.remove(&community_id); - // remove local cached community state so leave persists across restarts - let mut engine = state.crdt_engine.lock().await; - engine.remove_community(&community_id)?; - drop(engine); - - let mut guard = state.pending_join_role_guard.lock().await; - guard.remove(&community_id); - - Ok(()) + Ok(()) + }) } #[tauri::command] pub async fn get_communities(state: State<'_, AppState>) -> Result, String> { - let engine = state.crdt_engine.lock().await; - let mut communities = Vec::new(); + ipc_log!("get_communities", { + let engine = state.crdt_engine.lock().await; + let mut communities = Vec::new(); - for id in engine.community_ids() { - if let Ok(meta) = engine.get_community_meta(&id) { - communities.push(meta); + for id in engine.community_ids() { + if let Ok(meta) = engine.get_community_meta(&id) { + communities.push(meta); + } } - } - Ok(communities) + Ok(communities) + }) } #[tauri::command] @@ -374,57 +383,59 @@ pub async fn create_channel( kind: Option, category_id: Option, ) -> Result { - let mut hasher = Sha256::new(); - hasher.update(community_id.as_bytes()); - hasher.update(name.as_bytes()); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - hasher.update(now.to_le_bytes()); - let hash = hasher.finalize(); - let channel_id = format!("ch_{}", &hex::encode(hash)[..12]); + ipc_log!("create_channel", { + let mut hasher = Sha256::new(); + hasher.update(community_id.as_bytes()); + hasher.update(name.as_bytes()); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + hasher.update(now.to_le_bytes()); + let hash = hasher.finalize(); + let channel_id = format!("ch_{}", &hex::encode(hash)[..12]); - let channel_kind = match kind.as_deref() { - Some("voice") | Some("Voice") => ChannelKind::Voice, - _ => ChannelKind::Text, - }; + let channel_kind = match kind.as_deref() { + Some("voice") | Some("Voice") => ChannelKind::Voice, + _ => ChannelKind::Text, + }; - let channel = ChannelMeta { - id: channel_id, - community_id: community_id.clone(), - name, - topic, - kind: channel_kind, - position: 0, - category_id, - }; + let channel = ChannelMeta { + id: channel_id, + community_id: community_id.clone(), + name, + topic, + kind: channel_kind, + position: 0, + category_id, + }; - let mut engine = state.crdt_engine.lock().await; - engine.create_channel(&community_id, &channel)?; - drop(engine); + let mut engine = state.crdt_engine.lock().await; + engine.create_channel(&community_id, &channel)?; + drop(engine); - // subscribe to the new channel's topics - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let msg_topic = gossip::topic_for_messages(&community_id, &channel.id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { topic: msg_topic }) - .await; + // subscribe to the new channel's topics + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let msg_topic = gossip::topic_for_messages(&community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { topic: msg_topic }) + .await; - let typing_topic = gossip::topic_for_typing(&community_id, &channel.id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { - topic: typing_topic, - }) - .await; - } + let typing_topic = gossip::topic_for_typing(&community_id, &channel.id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { + topic: typing_topic, + }) + .await; + } - broadcast_sync(&state, &community_id).await; + broadcast_sync(&state, &community_id).await; - Ok(channel) + Ok(channel) + }) } #[tauri::command] @@ -432,8 +443,10 @@ pub async fn get_channels( state: State<'_, AppState>, community_id: String, ) -> Result, String> { - let engine = state.crdt_engine.lock().await; - engine.get_channels(&community_id) + ipc_log!("get_channels", { + let engine = state.crdt_engine.lock().await; + engine.get_channels(&community_id) + }) } #[tauri::command] @@ -442,31 +455,33 @@ pub async fn create_category( community_id: String, name: String, ) -> Result { - let mut hasher = Sha256::new(); - hasher.update(community_id.as_bytes()); - hasher.update(name.as_bytes()); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; - hasher.update(now.to_le_bytes()); - let hash = hasher.finalize(); - let category_id = format!("cat_{}", &hex::encode(hash)[..12]); + ipc_log!("create_category", { + let mut hasher = Sha256::new(); + hasher.update(community_id.as_bytes()); + hasher.update(name.as_bytes()); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + hasher.update(now.to_le_bytes()); + let hash = hasher.finalize(); + let category_id = format!("cat_{}", &hex::encode(hash)[..12]); - let category = CategoryMeta { - id: category_id, - community_id: community_id.clone(), - name, - position: 0, - }; + let category = CategoryMeta { + id: category_id, + community_id: community_id.clone(), + name, + position: 0, + }; - let mut engine = state.crdt_engine.lock().await; - engine.create_category(&community_id, &category)?; - drop(engine); + let mut engine = state.crdt_engine.lock().await; + engine.create_category(&community_id, &category)?; + drop(engine); - broadcast_sync(&state, &community_id).await; + broadcast_sync(&state, &community_id).await; - Ok(category) + Ok(category) + }) } #[tauri::command] diff --git a/src-tauri/src/commands/dm.rs b/src-tauri/src/commands/dm.rs index b1e4b74..fcabd93 100644 --- a/src-tauri/src/commands/dm.rs +++ b/src-tauri/src/commands/dm.rs @@ -2,6 +2,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use tauri::State; +use super::ipc_log; use crate::node::gossip; use crate::node::NodeCommand; use crate::protocol::messages::{ @@ -19,103 +20,105 @@ pub async fn send_dm( peer_id: String, content: String, ) -> Result { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; + ipc_log!("send_dm", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; - let local_peer_id = id.peer_id.to_string(); - let display_name = id.display_name.clone(); - drop(identity); + let local_peer_id = id.peer_id.to_string(); + let display_name = id.display_name.clone(); + drop(identity); - let msg = DirectMessage { - id: format!("dm_{}_{}", local_peer_id, now), - from_peer: local_peer_id.clone(), - to_peer: peer_id.clone(), - from_display_name: display_name.clone(), - content: content.clone(), - timestamp: now, - }; + let msg = DirectMessage { + id: format!("dm_{}_{}", local_peer_id, now), + from_peer: local_peer_id.clone(), + to_peer: peer_id.clone(), + from_display_name: display_name.clone(), + content: content.clone(), + timestamp: now, + }; - // derive the conversation id and persist the message - let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); + // derive the conversation id and persist the message + let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); - state - .storage - .append_dm_message(&conversation_id, &msg) - .map_err(|e| format!("failed to persist dm: {}", e))?; + state + .storage + .append_dm_message(&conversation_id, &msg) + .map_err(|e| format!("failed to persist dm: {}", e))?; - // ensure conversation metadata exists on disk - // try to load existing meta to preserve peer's display name, - // fall back to what we know from the directory - let existing_meta = state.storage.load_dm_conversation(&conversation_id).ok(); - let peer_display_name = existing_meta - .as_ref() - .map(|m| m.display_name.clone()) - .unwrap_or_else(|| { - // look up in directory - state - .storage - .load_directory() - .ok() - .and_then(|d| d.get(&peer_id).map(|e| e.display_name.clone())) - .unwrap_or_else(|| peer_id.clone()) - }); + // ensure conversation metadata exists on disk + // try to load existing meta to preserve peer's display name, + // fall back to what we know from the directory + let existing_meta = state.storage.load_dm_conversation(&conversation_id).ok(); + let peer_display_name = existing_meta + .as_ref() + .map(|m| m.display_name.clone()) + .unwrap_or_else(|| { + // look up in directory + state + .storage + .load_directory() + .ok() + .and_then(|d| d.get(&peer_id).map(|e| e.display_name.clone())) + .unwrap_or_else(|| peer_id.clone()) + }); - let meta = DMConversationMeta { - peer_id: peer_id.clone(), - display_name: peer_display_name, - last_message: Some(content), - last_message_time: Some(now), - unread_count: existing_meta.map(|m| m.unread_count).unwrap_or(0), - }; + let meta = DMConversationMeta { + peer_id: peer_id.clone(), + display_name: peer_display_name, + last_message: Some(content), + last_message_time: Some(now), + unread_count: existing_meta.map(|m| m.unread_count).unwrap_or(0), + }; - state - .storage - .save_dm_conversation(&conversation_id, &meta) - .map_err(|e| format!("failed to save dm conversation: {}", e))?; + state + .storage + .save_dm_conversation(&conversation_id, &meta) + .map_err(|e| format!("failed to save dm conversation: {}", e))?; - // publish to the dm gossipsub topic - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let data = serde_json::to_vec(&GossipMessage::DirectMessage(msg.clone())) - .map_err(|e| format!("serialize error: {}", e))?; + // publish to the dm gossipsub topic + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let data = serde_json::to_vec(&GossipMessage::DirectMessage(msg.clone())) + .map_err(|e| format!("serialize error: {}", e))?; - // publish to the pair topic (for when both peers are already subscribed) - let pair_topic = gossip::topic_for_dm(&local_peer_id, &peer_id); - let _ = handle - .command_tx - .send(NodeCommand::SendMessage { - topic: pair_topic, - data: data.clone(), - }) - .await; + // publish to the pair topic (for when both peers are already subscribed) + let pair_topic = gossip::topic_for_dm(&local_peer_id, &peer_id); + let _ = handle + .command_tx + .send(NodeCommand::SendMessage { + topic: pair_topic, + data: data.clone(), + }) + .await; - // also publish to the recipient's inbox topic to guarantee delivery - // on first-time dms where the peer isn't subscribed to the pair topic yet - let inbox_topic = gossip::topic_for_dm_inbox(&peer_id); - let _ = handle - .command_tx - .send(NodeCommand::SendMessage { - topic: inbox_topic, - data, - }) - .await; + // also publish to the recipient's inbox topic to guarantee delivery + // on first-time dms where the peer isn't subscribed to the pair topic yet + let inbox_topic = gossip::topic_for_dm_inbox(&peer_id); + let _ = handle + .command_tx + .send(NodeCommand::SendMessage { + topic: inbox_topic, + data, + }) + .await; - // discover the peer via rendezvous in case we're not connected over wan - let discover_ns = format!("dusk/peer/{}", peer_id); - let _ = handle - .command_tx - .send(NodeCommand::DiscoverRendezvous { - namespace: discover_ns, - }) - .await; - } + // discover the peer via rendezvous in case we're not connected over wan + let discover_ns = format!("dusk/peer/{}", peer_id); + let _ = handle + .command_tx + .send(NodeCommand::DiscoverRendezvous { + namespace: discover_ns, + }) + .await; + } - Ok(msg) + Ok(msg) + }) } // load dm messages for a conversation with a specific peer @@ -126,17 +129,19 @@ pub async fn get_dm_messages( before: Option, limit: Option, ) -> Result, String> { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - let local_peer_id = id.peer_id.to_string(); - drop(identity); + ipc_log!("get_dm_messages", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + let local_peer_id = id.peer_id.to_string(); + drop(identity); - let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); + let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); - state - .storage - .load_dm_messages(&conversation_id, before, limit.unwrap_or(50)) - .map_err(|e| format!("failed to load dm messages: {}", e)) + state + .storage + .load_dm_messages(&conversation_id, before, limit.unwrap_or(50)) + .map_err(|e| format!("failed to load dm messages: {}", e)) + }) } // search dm messages on the backend using sqlite indexes @@ -152,33 +157,35 @@ pub async fn search_dm_messages( date_before: Option, limit: Option, ) -> Result, String> { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - let local_peer_id = id.peer_id.to_string(); - drop(identity); + ipc_log!("search_dm_messages", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + let local_peer_id = id.peer_id.to_string(); + drop(identity); - let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); + let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); - let from_peer = match from_filter.as_deref() { - Some("me") => Some(local_peer_id), - Some("them") => Some(peer_id.clone()), - _ => None, - }; + let from_peer = match from_filter.as_deref() { + Some("me") => Some(local_peer_id), + Some("them") => Some(peer_id.clone()), + _ => None, + }; - let params = DmSearchParams { - query, - from_peer, - media_filter, - mentions_only: mentions_only.unwrap_or(false), - date_after, - date_before, - limit: limit.unwrap_or(200), - }; + let params = DmSearchParams { + query, + from_peer, + media_filter, + mentions_only: mentions_only.unwrap_or(false), + date_after, + date_before, + limit: limit.unwrap_or(200), + }; - state - .storage - .search_dm_messages(&conversation_id, ¶ms) - .map_err(|e| format!("failed to search dm messages: {}", e)) + state + .storage + .search_dm_messages(&conversation_id, ¶ms) + .map_err(|e| format!("failed to search dm messages: {}", e)) + }) } // load all dm conversations for the sidebar @@ -186,35 +193,39 @@ pub async fn search_dm_messages( pub async fn get_dm_conversations( state: State<'_, AppState>, ) -> Result, String> { - let conversations = state - .storage - .load_all_dm_conversations() - .map_err(|e| format!("failed to load dm conversations: {}", e))?; + ipc_log!("get_dm_conversations", { + let conversations = state + .storage + .load_all_dm_conversations() + .map_err(|e| format!("failed to load dm conversations: {}", e))?; - Ok(conversations.into_iter().map(|(_, meta)| meta).collect()) + Ok(conversations.into_iter().map(|(_, meta)| meta).collect()) + }) } // mark all messages in a dm conversation as read #[tauri::command] pub async fn mark_dm_read(state: State<'_, AppState>, peer_id: String) -> Result<(), String> { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - let local_peer_id = id.peer_id.to_string(); - drop(identity); + ipc_log!("mark_dm_read", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + let local_peer_id = id.peer_id.to_string(); + drop(identity); - let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); + let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); - let mut meta = state - .storage - .load_dm_conversation(&conversation_id) - .map_err(|e| format!("failed to load conversation: {}", e))?; + let mut meta = state + .storage + .load_dm_conversation(&conversation_id) + .map_err(|e| format!("failed to load conversation: {}", e))?; - meta.unread_count = 0; + meta.unread_count = 0; - state - .storage - .save_dm_conversation(&conversation_id, &meta) - .map_err(|e| format!("failed to save conversation: {}", e)) + state + .storage + .save_dm_conversation(&conversation_id, &meta) + .map_err(|e| format!("failed to save conversation: {}", e)) + }) } // delete a dm conversation and all its messages @@ -223,61 +234,65 @@ pub async fn delete_dm_conversation( state: State<'_, AppState>, peer_id: String, ) -> Result<(), String> { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - let local_peer_id = id.peer_id.to_string(); - drop(identity); + ipc_log!("delete_dm_conversation", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + let local_peer_id = id.peer_id.to_string(); + drop(identity); - let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); + let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); - // unsubscribe from the dm topic - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); - let _ = handle - .command_tx - .send(NodeCommand::Unsubscribe { topic }) - .await; - } + // unsubscribe from the dm topic + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); + let _ = handle + .command_tx + .send(NodeCommand::Unsubscribe { topic }) + .await; + } - state - .storage - .remove_dm_conversation(&conversation_id) - .map_err(|e| format!("failed to delete conversation: {}", e)) + state + .storage + .remove_dm_conversation(&conversation_id) + .map_err(|e| format!("failed to delete conversation: {}", e)) + }) } // send a typing indicator in a dm conversation #[tauri::command] pub async fn send_dm_typing(state: State<'_, AppState>, peer_id: String) -> Result<(), String> { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - let local_peer_id = id.peer_id.to_string(); - drop(identity); + ipc_log!("send_dm_typing", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + let local_peer_id = id.peer_id.to_string(); + drop(identity); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u64; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u64; - let indicator = DMTypingIndicator { - from_peer: local_peer_id.clone(), - to_peer: peer_id.clone(), - timestamp: now, - }; + let indicator = DMTypingIndicator { + from_peer: local_peer_id.clone(), + to_peer: peer_id.clone(), + timestamp: now, + }; - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); - let data = serde_json::to_vec(&GossipMessage::DMTyping(indicator)) - .map_err(|e| format!("serialize error: {}", e))?; + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); + let data = serde_json::to_vec(&GossipMessage::DMTyping(indicator)) + .map_err(|e| format!("serialize error: {}", e))?; - let _ = handle - .command_tx - .send(NodeCommand::SendMessage { topic, data }) - .await; - } + let _ = handle + .command_tx + .send(NodeCommand::SendMessage { topic, data }) + .await; + } - Ok(()) + Ok(()) + }) } // open a dm conversation with a peer (creates metadata on disk and subscribes to topic) @@ -288,16 +303,51 @@ pub async fn open_dm_conversation( peer_id: String, display_name: String, ) -> Result { - let identity = state.identity.lock().await; - let id = identity.as_ref().ok_or("no identity loaded")?; - let local_peer_id = id.peer_id.to_string(); - drop(identity); + ipc_log!("open_dm_conversation", { + let identity = state.identity.lock().await; + let id = identity.as_ref().ok_or("no identity loaded")?; + let local_peer_id = id.peer_id.to_string(); + drop(identity); - let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); + let conversation_id = gossip::dm_conversation_id(&local_peer_id, &peer_id); - // check if conversation already exists - if let Ok(existing) = state.storage.load_dm_conversation(&conversation_id) { - // subscribe to make sure we're listening + // check if conversation already exists + if let Ok(existing) = state.storage.load_dm_conversation(&conversation_id) { + // subscribe to make sure we're listening + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); + let _ = handle + .command_tx + .send(NodeCommand::Subscribe { topic }) + .await; + + // discover the peer via rendezvous to ensure wan connectivity + let discover_ns = format!("dusk/peer/{}", peer_id); + let _ = handle + .command_tx + .send(NodeCommand::DiscoverRendezvous { + namespace: discover_ns, + }) + .await; + } + return Ok(existing); + } + + let meta = DMConversationMeta { + peer_id: peer_id.clone(), + display_name, + last_message: None, + last_message_time: None, + unread_count: 0, + }; + + state + .storage + .save_dm_conversation(&conversation_id, &meta) + .map_err(|e| format!("failed to create dm conversation: {}", e))?; + + // subscribe to the dm topic so we receive messages let node_handle = state.node_handle.lock().await; if let Some(ref handle) = *node_handle { let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); @@ -306,7 +356,8 @@ pub async fn open_dm_conversation( .send(NodeCommand::Subscribe { topic }) .await; - // discover the peer via rendezvous to ensure wan connectivity + // discover the peer via rendezvous to establish wan connectivity + // through the relay circuit before any messages are sent let discover_ns = format!("dusk/peer/{}", peer_id); let _ = handle .command_tx @@ -315,41 +366,7 @@ pub async fn open_dm_conversation( }) .await; } - return Ok(existing); - } - let meta = DMConversationMeta { - peer_id: peer_id.clone(), - display_name, - last_message: None, - last_message_time: None, - unread_count: 0, - }; - - state - .storage - .save_dm_conversation(&conversation_id, &meta) - .map_err(|e| format!("failed to create dm conversation: {}", e))?; - - // subscribe to the dm topic so we receive messages - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let topic = gossip::topic_for_dm(&local_peer_id, &peer_id); - let _ = handle - .command_tx - .send(NodeCommand::Subscribe { topic }) - .await; - - // discover the peer via rendezvous to establish wan connectivity - // through the relay circuit before any messages are sent - let discover_ns = format!("dusk/peer/{}", peer_id); - let _ = handle - .command_tx - .send(NodeCommand::DiscoverRendezvous { - namespace: discover_ns, - }) - .await; - } - - Ok(meta) + Ok(meta) + }) } diff --git a/src-tauri/src/commands/identity.rs b/src-tauri/src/commands/identity.rs index e219748..4bb87a3 100644 --- a/src-tauri/src/commands/identity.rs +++ b/src-tauri/src/commands/identity.rs @@ -11,6 +11,8 @@ use crate::storage::UserSettings; use crate::verification::{self, ChallengeSubmission}; use crate::AppState; +use super::ipc_log; + // build a signed profile announcement and publish it on the directory topic // so all connected peers immediately learn about the updated profile. // silently no-ops if the node isn't running yet. @@ -46,25 +48,27 @@ async fn announce_profile(id: &DuskIdentity, state: &AppState) { #[tauri::command] pub async fn has_identity(state: State<'_, AppState>) -> Result { - Ok(state.storage.has_identity()) + ipc_log!("has_identity", Ok(state.storage.has_identity())) } #[tauri::command] pub async fn load_identity(state: State<'_, AppState>) -> Result, String> { - let mut identity = state.identity.lock().await; + ipc_log!("load_identity", { + let mut identity = state.identity.lock().await; - if identity.is_some() { - return Ok(identity.as_ref().map(|id| id.public_identity())); - } - - match DuskIdentity::load(&state.storage) { - Ok(loaded) => { - let public = loaded.public_identity(); - *identity = Some(loaded); - Ok(Some(public)) + if identity.is_some() { + Ok(identity.as_ref().map(|id| id.public_identity())) + } else { + match DuskIdentity::load(&state.storage) { + Ok(loaded) => { + let public = loaded.public_identity(); + *identity = Some(loaded); + Ok(Some(public)) + } + Err(_) => Ok(None), + } } - Err(_) => Ok(None), - } + }) } #[tauri::command] @@ -74,56 +78,61 @@ pub async fn create_identity( bio: Option, challenge_data: Option, ) -> Result { - // require challenge data and re-validate behavioral analysis in rust - let challenge = challenge_data.ok_or("verification required")?; - let result = verification::analyze_challenge(&challenge); - if !result.is_human { - return Err("verification failed".to_string()); - } + ipc_log!("create_identity", { + // require challenge data and re-validate behavioral analysis in rust + let challenge = challenge_data.ok_or("verification required")?; + let result = verification::analyze_challenge(&challenge); + if !result.is_human { + Err("verification failed".to_string()) + } else { + let mut new_identity = + DuskIdentity::generate(&display_name, &bio.unwrap_or_default()); - let mut new_identity = DuskIdentity::generate(&display_name, &bio.unwrap_or_default()); + // generate a cryptographic proof binding the verification to this keypair + let proof = verification::generate_proof( + &challenge, + &new_identity.keypair, + &new_identity.peer_id.to_string(), + )?; - // generate a cryptographic proof binding the verification to this keypair - let proof = verification::generate_proof( - &challenge, - &new_identity.keypair, - &new_identity.peer_id.to_string(), - )?; + state + .storage + .save_verification_proof(&proof) + .map_err(|e| format!("failed to save verification proof: {}", e))?; - state - .storage - .save_verification_proof(&proof) - .map_err(|e| format!("failed to save verification proof: {}", e))?; + new_identity.verification_proof = Some(proof); + new_identity.save(&state.storage)?; - new_identity.verification_proof = Some(proof); - new_identity.save(&state.storage)?; + // also save initial settings with this display name so they're in sync + let mut settings = state.storage.load_settings().unwrap_or_default(); + settings.display_name = display_name.clone(); + state + .storage + .save_settings(&settings) + .map_err(|e| format!("failed to save initial settings: {}", e))?; - // also save initial settings with this display name so they're in sync - let mut settings = state.storage.load_settings().unwrap_or_default(); - settings.display_name = display_name.clone(); - state - .storage - .save_settings(&settings) - .map_err(|e| format!("failed to save initial settings: {}", e))?; + let public = new_identity.public_identity(); + let mut identity = state.identity.lock().await; + *identity = Some(new_identity); - let public = new_identity.public_identity(); - let mut identity = state.identity.lock().await; - *identity = Some(new_identity); - - Ok(public) + Ok(public) + } + }) } #[tauri::command] pub async fn update_display_name(state: State<'_, AppState>, name: String) -> Result<(), String> { - let mut identity = state.identity.lock().await; - let id = identity.as_mut().ok_or("no identity loaded")?; + ipc_log!("update_display_name", { + let mut identity = state.identity.lock().await; + let id = identity.as_mut().ok_or("no identity loaded")?; - id.display_name = name; - id.save(&state.storage)?; + id.display_name = name; + id.save(&state.storage)?; - announce_profile(id, &state).await; + announce_profile(id, &state).await; - Ok(()) + Ok(()) + }) } #[tauri::command] @@ -132,27 +141,31 @@ pub async fn update_profile( display_name: String, bio: String, ) -> Result { - let mut identity = state.identity.lock().await; - let id = identity.as_mut().ok_or("no identity loaded")?; + ipc_log!("update_profile", { + let mut identity = state.identity.lock().await; + let id = identity.as_mut().ok_or("no identity loaded")?; - id.display_name = display_name; - id.bio = bio; - id.save(&state.storage)?; + id.display_name = display_name; + id.bio = bio; + id.save(&state.storage)?; - let public = id.public_identity(); + let public = id.public_identity(); - // re-announce so connected peers see the change immediately - announce_profile(id, &state).await; + // re-announce so connected peers see the change immediately + announce_profile(id, &state).await; - Ok(public) + Ok(public) + }) } #[tauri::command] pub async fn load_settings(state: State<'_, AppState>) -> Result { - state - .storage - .load_settings() - .map_err(|e| format!("failed to load settings: {}", e)) + ipc_log!("load_settings", { + state + .storage + .load_settings() + .map_err(|e| format!("failed to load settings: {}", e)) + }) } #[tauri::command] @@ -160,75 +173,79 @@ pub async fn save_settings( state: State<'_, AppState>, settings: UserSettings, ) -> Result<(), String> { - // check if status changed so we can broadcast the new presence - let old_status = state - .storage - .load_settings() - .map(|s| s.status) - .unwrap_or_else(|_| "online".to_string()); - let status_changed = old_status != settings.status; + ipc_log!("save_settings", { + // check if status changed so we can broadcast the new presence + let old_status = state + .storage + .load_settings() + .map(|s| s.status) + .unwrap_or_else(|_| "online".to_string()); + let status_changed = old_status != settings.status; - // also update the identity display name if it changed - let mut identity = state.identity.lock().await; - let mut name_changed = false; - if let Some(id) = identity.as_mut() { - if id.display_name != settings.display_name { - id.display_name = settings.display_name.clone(); - id.save(&state.storage)?; - name_changed = true; + // also update the identity display name if it changed + let mut identity = state.identity.lock().await; + let mut name_changed = false; + if let Some(id) = identity.as_mut() { + if id.display_name != settings.display_name { + id.display_name = settings.display_name.clone(); + id.save(&state.storage)?; + name_changed = true; + } } - } - // re-announce if the display name was updated through settings - if name_changed { - if let Some(id) = identity.as_ref() { - announce_profile(id, &state).await; + // re-announce if the display name was updated through settings + if name_changed { + if let Some(id) = identity.as_ref() { + announce_profile(id, &state).await; + } } - } - drop(identity); + drop(identity); - // broadcast presence if status changed - if status_changed { - use crate::node::NodeCommand; - use crate::protocol::messages::PeerStatus; + // broadcast presence if status changed + if status_changed { + use crate::node::NodeCommand; + use crate::protocol::messages::PeerStatus; - let peer_status = match settings.status.as_str() { - "idle" => PeerStatus::Idle, - "dnd" => PeerStatus::Dnd, - "invisible" => PeerStatus::Offline, - _ => PeerStatus::Online, - }; + let peer_status = match settings.status.as_str() { + "idle" => PeerStatus::Idle, + "dnd" => PeerStatus::Dnd, + "invisible" => PeerStatus::Offline, + _ => PeerStatus::Online, + }; - let node_handle = state.node_handle.lock().await; - if let Some(ref handle) = *node_handle { - let _ = handle - .command_tx - .send(NodeCommand::BroadcastPresence { - status: peer_status, - }) - .await; + let node_handle = state.node_handle.lock().await; + if let Some(ref handle) = *node_handle { + let _ = handle + .command_tx + .send(NodeCommand::BroadcastPresence { + status: peer_status, + }) + .await; + } } - } - state - .storage - .save_settings(&settings) - .map_err(|e| format!("failed to save settings: {}", e)) + state + .storage + .save_settings(&settings) + .map_err(|e| format!("failed to save settings: {}", e)) + }) } // -- user directory commands -- #[tauri::command] pub async fn get_known_peers(state: State<'_, AppState>) -> Result, String> { - let entries = state - .storage - .load_directory() - .map_err(|e| format!("failed to load directory: {}", e))?; + ipc_log!("get_known_peers", { + let entries = state + .storage + .load_directory() + .map_err(|e| format!("failed to load directory: {}", e))?; - let mut peers: Vec = entries.into_values().collect(); - // sort by last seen (most recent first) - peers.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); - Ok(peers) + let mut peers: Vec = entries.into_values().collect(); + // sort by last seen (most recent first) + peers.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); + Ok(peers) + }) } #[tauri::command] @@ -236,50 +253,56 @@ pub async fn search_directory( state: State<'_, AppState>, query: String, ) -> Result, String> { - let entries = state - .storage - .load_directory() - .map_err(|e| format!("failed to load directory: {}", e))?; + ipc_log!("search_directory", { + let entries = state + .storage + .load_directory() + .map_err(|e| format!("failed to load directory: {}", e))?; - let query_lower = query.to_lowercase(); - let mut results: Vec = entries - .into_values() - .filter(|entry| { - entry.display_name.to_lowercase().contains(&query_lower) - || entry.peer_id.to_lowercase().contains(&query_lower) - }) - .collect(); + let query_lower = query.to_lowercase(); + let mut results: Vec = entries + .into_values() + .filter(|entry| { + entry.display_name.to_lowercase().contains(&query_lower) + || entry.peer_id.to_lowercase().contains(&query_lower) + }) + .collect(); - results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); - Ok(results) + results.sort_by(|a, b| b.last_seen.cmp(&a.last_seen)); + Ok(results) + }) } #[tauri::command] pub async fn get_friends(state: State<'_, AppState>) -> Result, String> { - let entries = state - .storage - .load_directory() - .map_err(|e| format!("failed to load directory: {}", e))?; + ipc_log!("get_friends", { + let entries = state + .storage + .load_directory() + .map_err(|e| format!("failed to load directory: {}", e))?; - let mut friends: Vec = entries - .into_values() - .filter(|entry| entry.is_friend) - .collect(); + let mut friends: Vec = entries + .into_values() + .filter(|entry| entry.is_friend) + .collect(); - friends.sort_by(|a, b| { - a.display_name - .to_lowercase() - .cmp(&b.display_name.to_lowercase()) - }); - Ok(friends) + friends.sort_by(|a, b| { + a.display_name + .to_lowercase() + .cmp(&b.display_name.to_lowercase()) + }); + Ok(friends) + }) } #[tauri::command] pub async fn add_friend(state: State<'_, AppState>, peer_id: String) -> Result<(), String> { - state - .storage - .set_friend_status(&peer_id, true) - .map_err(|e| format!("failed to add friend: {}", e)) + ipc_log!("add_friend", { + state + .storage + .set_friend_status(&peer_id, true) + .map_err(|e| format!("failed to add friend: {}", e)) + }) } #[tauri::command] diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 4d35162..30ea45d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,20 @@ +// logs every tauri ipc command invocation and its result to the terminal +macro_rules! ipc_log { + ($cmd:expr, $body:expr) => {{ + let start = std::time::Instant::now(); + log::info!("[ipc] -> {}", $cmd); + let result = $body; + let elapsed = start.elapsed(); + match &result { + Ok(_) => log::info!("[ipc] <- {} ok ({:.1?})", $cmd, elapsed), + Err(e) => log::error!("[ipc] <- {} err ({:.1?}): {}", $cmd, elapsed, e), + } + result + }}; +} + +pub(crate) use ipc_log; + pub mod chat; pub mod community; pub mod dm; diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index ca342cb..24dfc1f 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { invoke as tauriInvoke } from "@tauri-apps/api/core"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import type { PublicIdentity, @@ -19,6 +19,42 @@ import type { GifResponse, } from "./types"; +// wrapped invoke that logs all ipc calls and errors +async function invoke(cmd: string, args?: Record): Promise { + const startTime = performance.now(); + const logPrefix = `[ipc] ${cmd}`; + + // log the call with args (redacting sensitive data) + const safeArgs = args ? redactSensitiveArgs(args) : undefined; + console.log(`${logPrefix} called`, safeArgs ? { args: safeArgs } : ""); + + try { + const result = await tauriInvoke(cmd, args); + const duration = (performance.now() - startTime).toFixed(2); + console.log(`${logPrefix} success (${duration}ms)`, result); + return result; + } catch (error) { + const duration = (performance.now() - startTime).toFixed(2); + console.error(`${logPrefix} error (${duration}ms)`, error); + throw error; + } +} + +// redact potentially sensitive values from args before logging +function redactSensitiveArgs(args: Record): Record { + const sensitiveKeys = ["password", "secret", "token", "key", "credential", "private"]; + const redacted = { ...args }; + + for (const key of Object.keys(redacted)) { + const lowerKey = key.toLowerCase(); + if (sensitiveKeys.some((sk) => lowerKey.includes(sk))) { + redacted[key] = "[REDACTED]"; + } + } + + return redacted; +} + // -- identity -- export async function hasIdentity(): Promise {