Browse Source

app/darkirc: send msgs to private channels

darkfi 3 weeks ago
parent
commit
4128d4ca73

+ 1 - 0
Cargo.toml

@@ -79,6 +79,7 @@ oxy-upnp-igd = {version = "0.1", optional = true}
 
 # Encoding
 bs58 = {version = "0.5.1", optional = true}
+crypto-box = {version = "0.2.1", optional = true}
 hex = {version = "0.4.3", optional = true}
 serde = {version = "1.0.228", features = ["derive"], optional = true}
 tinyjson = {version = "2.5.1", optional = true}

+ 1 - 0
bin/app/Cargo.lock

@@ -1861,6 +1861,7 @@ dependencies = [
  "chrono",
  "clap 4.6.1",
  "colored",
+ "crypto_box",
  "darkfi",
  "darkfi-sdk",
  "darkfi-serial",

+ 2 - 1
bin/app/Cargo.toml

@@ -20,6 +20,7 @@ glam = "0.30.9"
 #async_zmq = "0.4.0"
 zeromq = { version = "0.4.1", default-features = false, features = ["async-std-runtime", "all-transport"] }
 darkirc = { path = "../darkirc", optional = true }
+crypto_box = { version = "0", optional = true }
 darkfi = {path = "../../", features = ["async-daemonize", "event-graph", "net", "util", "system", "tx", "zk"]}
 darkfi-sdk = {path = "../../src/sdk", features = ["async"]}
 darkfi-serial = {path = "../../src/serial", features = ["async"]}
@@ -74,7 +75,7 @@ enable-filelog = []
 # Network debugging. Should be disabled in release.
 enable-netdebug = []
 
-enable-plugin-darkirc = ["dep:darkirc"]
+enable-plugin-darkirc = ["dep:darkirc", "dep:crypto_box"]
 enable-plugin-fud = ["dep:fud"]
 enable-plugin-drk = ["dep:drk"]
 enable-plugins = ["enable-plugin-darkirc", "enable-plugin-fud", "enable-plugin-drk"]

+ 1 - 1
bin/app/src/app/mod.rs

@@ -39,7 +39,7 @@ pub mod locale;
 use locale::read_locale_ftl;
 mod node;
 use node::create_window;
-mod schema;
+pub mod schema;
 use schema::get_settingsdb_path;
 
 macro_rules! d { ($($arg:tt)*) => { debug!(target: "app", $($arg)*); } }

+ 60 - 45
bin/app/src/app/schema/menu/channel.rs

@@ -16,6 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+use bs58;
 use darkfi_serial::{async_trait, deserialize, Encodable, SerialDecodable, SerialEncodable};
 use sled_overlay::sled;
 use ui_consts::*;
@@ -28,7 +29,7 @@ macro_rules! e { ($($arg:tt)*) => { error!(target: "app::channel", $($arg)*); }
 #[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
 pub struct Channel {
     pub name: String,
-    pub secret: Option<String>,
+    pub secret: Option<[u8; 32]>,
 }
 
 use super::{
@@ -1151,14 +1152,14 @@ pub async fn make(
     let (slot, recvr) = Slot::new("receive_copy_clicked");
     node.register("click", slot).unwrap();
     let secedit_node2 = secedit_node.clone();
-    let renderer_clone = app.renderer.clone();
+    let renderer2 = app.renderer.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             debug!(target: "app::menu", "secret paste button clicked");
             match miniquad::window::clipboard_get() {
                 Some(clipboard_text) => {
                     let text_prop = secedit_node2.get_property("text").unwrap();
-                    let atom = &mut renderer_clone.make_guard(gfxtag!("secret paste"));
+                    let atom = &mut renderer2.make_guard(gfxtag!("secret paste"));
                     text_prop.set_str(atom, Role::App, 0, &clipboard_text).unwrap();
                     if let crate::scene::Pimpl::Edit(edit) = secedit_node2.pimpl() {
                         edit.on_text_prop_changed();
@@ -1322,55 +1323,67 @@ pub async fn make(
     content_area.link(menu_node.clone());
 
     // Setup add_channel button handler now that menu_node exists
-    let channels_tree_clone = channels_tree.clone();
-    let nickedit_clone = nickedit_node.clone();
-    let secedit_clone = secedit_node.clone();
-    let menu_prop_clone = menu_node.get_property("items").unwrap();
-    let renderer_clone = app.renderer.clone();
+    let channels_tree2 = channels_tree.clone();
+    let nickedit2 = nickedit_node.clone();
+    let secedit2 = secedit_node.clone();
+    let menu_prop2 = menu_node.get_property("items").unwrap();
+    let renderer2 = app.renderer.clone();
+    let sg_root2 = app.sg_root.clone();
 
     let save_channel = app.ex.spawn(async move {
         while let Ok(_) = addchannel_recvr.recv().await {
-            let name_prop = nickedit_clone.get_property("text").unwrap();
+            let name_prop = nickedit2.get_property("text").unwrap();
             let name = name_prop.get_str(0).unwrap_or_default();
 
-            let secret_prop = secedit_clone.get_property("text").unwrap();
+            let secret_prop = secedit2.get_property("text").unwrap();
             let secret = secret_prop.get_str(0).unwrap_or_default();
 
             if name.is_empty() {
                 w!("Attempted to add channel with empty name");
                 continue;
             }
+            // TODO: Do more thorough checking of channel names
 
             let name = if name.starts_with('#') { name.trim_start_matches('#') } else { &name };
 
             let channel_name = format!("#{}", name);
 
-            let channel = Channel {
-                name: name.to_string(),
-                secret: if secret.is_empty() { None } else { Some(secret.clone()) },
-            };
+            let mut channel = Channel { name: name.to_string(), secret: None };
+
+            // Try to decode secret field if it is set
+            if !secret.is_empty() {
+                let Ok(bytes) = bs58::decode(&secret).into_vec() else {
+                    w!("Failed to decode secret base58");
+                    continue
+                };
+                if bytes.len() != 32 {
+                    w!("Invalid secret length: {} bytes (expected 32)", bytes.len());
+                    continue
+                }
 
-            let mut val = vec![];
-            if let Err(e) = channel.encode(&mut val) {
-                e!("Failed to serialize channel: {e}");
-                continue;
+                let mut arr = [0u8; 32];
+                arr.copy_from_slice(&bytes);
+                channel.secret = Some(arr);
             }
 
+            let mut val = vec![];
+            channel.encode(&mut val).unwrap();
+
             let key = name;
 
-            if let Err(e) = channels_tree_clone.insert(key, val) {
-                e!("Failed to save channel: {e}");
-                continue;
-            }
+            channels_tree2.insert(key, val).unwrap();
+            let _ = channels_tree2.flush_async().await;
 
-            let _ = channels_tree_clone.flush_async().await;
+            // Notify darkirc2 to reload channels
+            let darkirc2 = sg_root2.lookup_node("/plugin/darkirc").unwrap();
+            darkirc2.call_method("reload", vec![]).await.unwrap();
 
-            let atom = &mut renderer_clone.make_guard(gfxtag!("add_channel"));
-            menu_prop_clone.push_str(atom, Role::App, &channel_name).unwrap();
+            let atom = &mut renderer2.make_guard(gfxtag!("add_channel"));
+            menu_prop2.push_str(atom, Role::App, &channel_name).unwrap();
 
             i!("Successfully saved channel: {}", channel_name);
 
-            let atom = &mut renderer_clone.make_guard(gfxtag!("clear_channel_fields"));
+            let atom = &mut renderer2.make_guard(gfxtag!("clear_channel_fields"));
             name_prop.set_str(atom, Role::App, 0, "").unwrap();
             secret_prop.set_str(atom, Role::App, 0, "").unwrap();
         }
@@ -1395,21 +1408,26 @@ pub async fn make(
     let sg_root = app.sg_root.clone();
     let renderer = app.renderer.clone();
     let ex = app.ex.clone();
-    let channels_tree_clone = channels_tree.clone();
-    let content_clone = content.clone();
+    let channels_tree2 = channels_tree.clone();
+    let content2 = content.clone();
     let channel_vis = channel_is_visible.clone();
-    let window_scale_clone = window_scale.clone();
-    let db_clone = db.clone();
-    let i18n_fish_clone = i18n_fish.clone();
-    let emoji_meshes_clone = emoji_meshes.clone();
+    let window_scale2 = window_scale.clone();
+    let db2 = db.clone();
+    let i18n_fish2 = i18n_fish.clone();
+    let emoji_meshes2 = emoji_meshes.clone();
 
     let listen_select = app.ex.spawn(async move {
         while let Ok(data) = recvr.recv().await {
-            let channel_name: String = deserialize(&data).unwrap();
-            let path = format!("/window/content/{}_chat_layer", &channel_name);
+            let channel: String = deserialize(&data).unwrap();
+            i!("Selected channel: {channel}");
+            let path = format!("/window/content/{}_chat_layer", &channel);
+
+            let atom = &mut renderer.make_guard(gfxtag!("channel_selected"));
 
             // Check if chat layer already exists
-            if sg_root.lookup_node(&path).is_some() {
+            if let Some(node) = sg_root.lookup_node(&path) {
+                node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
+                channel_vis.set(atom, false);
                 continue;
             }
 
@@ -1418,31 +1436,28 @@ pub async fn make(
                 &sg_root,
                 &renderer,
                 &ex,
-                content_clone.clone(),
-                &channel_name,
-                &db_clone,
-                &i18n_fish_clone,
-                emoji_meshes_clone.clone(),
+                content2.clone(),
+                &channel,
+                &db2,
+                &i18n_fish2,
+                emoji_meshes2.clone(),
                 is_first_time,
             )
             .await;
 
             // Show the chat layer
-            let atom = &mut renderer.make_guard(gfxtag!("channel_selected"));
             node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
 
             // Add to main_menu items (check if not already there)
             let main_menu = sg_root.lookup_node("/window/content/menu_layer/main_menu").unwrap();
             let items_prop = main_menu.get_property("items").unwrap();
 
-            if !items_prop.contains_str(&channel_name) {
-                items_prop.push_str(atom, Role::App, &channel_name).unwrap();
+            if !items_prop.contains_str(&channel) {
+                items_prop.push_str(atom, Role::App, &channel).unwrap();
             }
 
             // Hide channel screen
             channel_vis.set(atom, false);
-
-            i!("Selected channel: {}", channel_name);
         }
     });
 

+ 1 - 1
bin/app/src/app/schema/mod.rs

@@ -37,7 +37,7 @@ use crate::{
 };
 
 mod chat;
-mod menu;
+pub mod menu;
 use menu::channel::Channel;
 //mod settings;
 pub mod test;

+ 2 - 0
bin/app/src/main.rs

@@ -774,6 +774,8 @@ pub fn create_darkirc(name: &str) -> SceneNode {
 
     node.add_method("reconnect", vec![], None).unwrap();
 
+    node.add_method("reload", vec![], None).unwrap();
+
     node
 }
 

+ 98 - 3
bin/app/src/plugin/darkirc2.rs

@@ -17,13 +17,15 @@
  */
 
 use std::{
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     io::Cursor,
     sync::{Arc, Mutex as SyncMutex, OnceLock, Weak},
     time::UNIX_EPOCH,
 };
 
 use async_lock::RwLock;
+use async_trait::async_trait;
+use crypto_box::{ChaChaBox, CryptoBox, PublicKey, SecretKey};
 use darkfi::{
     event_graph::{
         self,
@@ -50,6 +52,7 @@ use irc2::{
 use sled_overlay::sled;
 
 use crate::{
+    app::schema::menu::channel::Channel,
     error::{Error, Result},
     prop::{BatchGuardPtr, PropertyAtomicGuard, PropertyStr, Role},
     scene::{MethodCallSub, Pimpl, SceneNode, SceneNodePtr, SceneNodeType, SceneNodeWeak, Slot},
@@ -167,6 +170,7 @@ pub struct DarkIrc2 {
     nick: PropertyStr,
     pub channels: RwLock<HashMap<String, IrcChannel>>,
     pub contacts: RwLock<HashMap<String, IrcContact>>,
+    channels_tree: sled::Tree,
     settings: PluginSettings,
 }
 
@@ -189,6 +193,7 @@ impl DarkIrc2 {
         };
 
         let setting_tree = db.open_tree("settings")?;
+        let channels_tree = db.open_tree("channels")?;
         let settings = PluginSettings { setting_root, sled_tree: setting_tree };
 
         let mut p2p_settings: NetSettings = Default::default();
@@ -285,9 +290,16 @@ impl DarkIrc2 {
 
             channels: RwLock::new(HashMap::new()),
             contacts: RwLock::new(HashMap::new()),
+            channels_tree,
 
             settings,
         });
+
+        // Load channels from database BEFORE starting P2P
+        if let Err(e) = self_.load_channels_from_db().await {
+            e!("Failed to load channels: {e}");
+        }
+
         self_.clone().start(sg_root, ex).await;
         Ok(Pimpl::DarkIrc2(self_))
     }
@@ -549,6 +561,77 @@ impl DarkIrc2 {
         }
     }
 
+    /// Load channels from UI database and populate encryption keys
+    pub async fn load_channels_from_db(&self) -> Result<()> {
+        use darkfi_serial::deserialize;
+
+        let mut channels = self.channels.write().await;
+
+        for item in self.channels_tree.iter() {
+            let (key, val) = item.map_err(|e| {
+                e!("Failed to read channel from database: {e}");
+                Error::SledDbErr
+            })?;
+            let channel_name = String::from_utf8_lossy(&key).to_string();
+
+            // Deserialize UI channel struct
+            let ui_channel = match deserialize::<Channel>(&val) {
+                Ok(ch) => ch,
+                Err(e) => {
+                    w!("Failed to deserialize channel {channel_name}: {e}");
+                    continue
+                }
+            };
+
+            // Convert to IrcChannel with encryption
+            let full_name = format!("#{}", channel_name);
+            let mut irc_channel =
+                IrcChannel { topic: String::new(), nicks: HashSet::new(), saltbox: None };
+
+            if let Some(secret) = ui_channel.secret {
+                // Convert secret array to CryptoBox
+                let public = PublicKey::from_bytes(secret);
+                let secret_key = SecretKey::from_bytes(secret);
+                let saltbox = CryptoBox::new(&public, &secret_key);
+
+                irc_channel.saltbox = Some(Arc::new(saltbox));
+            }
+
+            let is_encrypted = irc_channel.saltbox.is_some();
+            channels.insert(full_name, irc_channel);
+            i!("Loaded channel: #{} (encrypted: {})", channel_name, is_encrypted);
+        }
+
+        Ok(())
+    }
+
+    /// Reload channels from database (called when UI adds/changes channels)
+    pub async fn reload(&self) -> Result<()> {
+        self.load_channels_from_db().await
+    }
+
+    async fn process_reload(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
+        let Ok(method_call) = sub.receive().await else {
+            d!("Reload method closed");
+            return false
+        };
+
+        t!("method called: reload({method_call:?})");
+
+        let Some(self_) = me.upgrade() else {
+            e!("DarkIrc2 destroyed before reload completed");
+            return false
+        };
+
+        if let Err(e) = self_.reload().await {
+            e!("Failed to reload channels: {e}");
+        } else {
+            i!("Successfully reloaded channels");
+        }
+
+        true
+    }
+
     async fn apply_settings(self_: Arc<Self>, _: BatchGuardPtr) {
         self_.settings.save_settings();
 
@@ -616,6 +699,11 @@ impl DarkIrc2 {
                 async move { while Self::process_reconnect(&me2, &reconnect_method_sub).await {} },
             );
 
+        let reload_method_sub = node.subscribe_method_call("reload").unwrap();
+        let me2 = me.clone();
+        let reload_method_task =
+            ex.spawn(async move { while Self::process_reload(&me2, &reload_method_sub).await {} });
+
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
         async fn save_nick(self_: Arc<DarkIrc2>, _batch: BatchGuardPtr) {
             let _ = std::fs::write(nick_filename(), self_.nick.get());
@@ -662,8 +750,15 @@ impl DarkIrc2 {
             }
         });
 
-        let mut tasks =
-            vec![send_method_task, reconnect_method_task, ev_task, dag_task, start_task, stop_task];
+        let mut tasks = vec![
+            send_method_task,
+            reconnect_method_task,
+            reload_method_task,
+            ev_task,
+            dag_task,
+            start_task,
+            stop_task,
+        ];
         tasks.append(&mut on_modify.tasks);
         self.tasks.set(tasks).unwrap();
     }