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