Переглянути джерело

wallet: attach the darkirc backend for a fully working chatapp now :D

darkfi 1 рік тому
батько
коміт
5c0db08286

+ 1 - 0
bin/darkwallet/Cargo.lock

@@ -1314,6 +1314,7 @@ dependencies = [
  "colored",
  "colored",
  "darkfi",
  "darkfi",
  "darkfi-serial",
  "darkfi-serial",
+ "dirs",
  "easy-parallel",
  "easy-parallel",
  "evgrd",
  "evgrd",
  "file-rotate",
  "file-rotate",

+ 1 - 0
bin/darkwallet/Cargo.toml

@@ -71,6 +71,7 @@ halo2_gadgets = {git="https://github.com/parazyd/halo2", branch="v4"}
 
 
 [target.'cfg(not(target_os = "android"))'.dependencies]
 [target.'cfg(not(target_os = "android"))'.dependencies]
 arboard = {version = "3.4.1", features=["wayland-data-control"]}
 arboard = {version = "3.4.1", features=["wayland-data-control"]}
+dirs = "5.0.1"
 
 
 [target.'cfg(target_os = "android")'.dependencies]
 [target.'cfg(target_os = "android")'.dependencies]
 android_logger = "0.13.3"
 android_logger = "0.13.3"

+ 12 - 7
bin/darkwallet/src/android.rs

@@ -17,7 +17,10 @@
  */
  */
 
 
 use miniquad::native::android::{self, ndk_sys, ndk_utils};
 use miniquad::native::android::{self, ndk_sys, ndk_utils};
-use std::sync::{LazyLock, Mutex as SyncMutex};
+use std::{
+    path::PathBuf,
+    sync::{LazyLock, Mutex as SyncMutex},
+};
 
 
 struct GlobalData {
 struct GlobalData {
     sender: Option<async_channel::Sender<AndroidSuggestEvent>>,
     sender: Option<async_channel::Sender<AndroidSuggestEvent>>,
@@ -95,8 +98,8 @@ pub fn get_keyboard_height() -> usize {
     }
     }
 }
 }
 
 
-pub fn get_appdata_path() -> String {
-    unsafe {
+pub fn get_appdata_path() -> PathBuf {
+    let path = unsafe {
         let env = android::attach_jni_env();
         let env = android::attach_jni_env();
 
 
         let text = ndk_utils::call_object_method!(
         let text = ndk_utils::call_object_method!(
@@ -106,10 +109,11 @@ pub fn get_appdata_path() -> String {
             "()Ljava/lang/String;"
             "()Ljava/lang/String;"
         );
         );
         ndk_utils::get_utf_str!(env, text).to_string()
         ndk_utils::get_utf_str!(env, text).to_string()
-    }
+    };
+    path.into()
 }
 }
-pub fn get_external_storage_path() -> String {
-    unsafe {
+pub fn get_external_storage_path() -> PathBuf {
+    let path = unsafe {
         let env = android::attach_jni_env();
         let env = android::attach_jni_env();
 
 
         let text = ndk_utils::call_object_method!(
         let text = ndk_utils::call_object_method!(
@@ -119,5 +123,6 @@ pub fn get_external_storage_path() -> String {
             "()Ljava/lang/String;"
             "()Ljava/lang/String;"
         );
         );
         ndk_utils::get_utf_str!(env, text).to_string()
         ndk_utils::get_utf_str!(env, text).to_string()
-    }
+    };
+    path.into()
 }
 }

+ 62 - 5
bin/darkwallet/src/app/mod.rs

@@ -19,28 +19,30 @@
 use async_recursion::async_recursion;
 use async_recursion::async_recursion;
 use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone};
 use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone};
 use darkfi::system::CondVar;
 use darkfi::system::CondVar;
-use darkfi_serial::Encodable;
+use darkfi_serial::{Decodable, Encodable};
 use futures::{stream::FuturesUnordered, StreamExt};
 use futures::{stream::FuturesUnordered, StreamExt};
 use sled_overlay::sled;
 use sled_overlay::sled;
 use smol::Task;
 use smol::Task;
 use std::{
 use std::{
+    io::Cursor,
     sync::{Arc, Mutex as SyncMutex},
     sync::{Arc, Mutex as SyncMutex},
     thread,
     thread,
 };
 };
 
 
 use crate::{
 use crate::{
-    darkirc::DarkIrcBackendPtr,
     error::Error,
     error::Error,
     expr::Op,
     expr::Op,
     gfx::{GraphicsEventPublisherPtr, RenderApi, Vertex},
     gfx::{GraphicsEventPublisherPtr, RenderApi, Vertex},
+    plugin::{self, PluginObject},
     prop::{Property, PropertyBool, PropertyStr, PropertySubType, PropertyType, Role},
     prop::{Property, PropertyBool, PropertyStr, PropertySubType, PropertyType, Role},
-    scene::{Pimpl, SceneNode as SceneNode3, SceneNodePtr, SceneNodeType as SceneNodeType3},
+    scene::{Pimpl, SceneNode as SceneNode3, SceneNodePtr, SceneNodeType as SceneNodeType3, Slot},
     text::TextShaperPtr,
     text::TextShaperPtr,
     ui::{chatview, Window},
     ui::{chatview, Window},
     ExecutorPtr,
     ExecutorPtr,
 };
 };
 
 
 mod node;
 mod node;
+use node::create_darkirc;
 mod schema;
 mod schema;
 
 
 //fn print_type_of<T>(_: &T) {
 //fn print_type_of<T>(_: &T) {
@@ -121,7 +123,6 @@ pub struct App {
     pub render_api: RenderApi,
     pub render_api: RenderApi,
     pub event_pub: GraphicsEventPublisherPtr,
     pub event_pub: GraphicsEventPublisherPtr,
     pub text_shaper: TextShaperPtr,
     pub text_shaper: TextShaperPtr,
-    pub darkirc_evgr: SyncMutex<Option<DarkIrcBackendPtr>>,
     pub tasks: SyncMutex<Vec<Task<()>>>,
     pub tasks: SyncMutex<Vec<Task<()>>>,
     pub ex: ExecutorPtr,
     pub ex: ExecutorPtr,
 }
 }
@@ -140,7 +141,6 @@ impl App {
             render_api,
             render_api,
             event_pub,
             event_pub,
             text_shaper,
             text_shaper,
-            darkirc_evgr: SyncMutex::new(None),
             tasks: SyncMutex::new(vec![]),
             tasks: SyncMutex::new(vec![]),
         })
         })
     }
     }
@@ -165,6 +165,55 @@ impl App {
         schema::make(&self, window).await;
         schema::make(&self, window).await;
 
 
         debug!(target: "app", "Schema loaded");
         debug!(target: "app", "Schema loaded");
+
+        let plugin = Arc::new(SceneNode3::new("plugin", SceneNodeType3::PluginRoot));
+        self.sg_root.clone().link(plugin.clone());
+
+        let darkirc = create_darkirc("darkirc");
+        let darkirc = darkirc
+            .setup(|me| async {
+                plugin::DarkIrc::new(me, self.ex.clone()).await.expect("DarkIrc pimpl setup")
+            })
+            .await;
+
+        let (slot, recvr) = Slot::new("recvmsg");
+        darkirc.register("recv", slot).unwrap();
+        let sg_root2 = self.sg_root.clone();
+        let listen_recv = self.ex.spawn(async move {
+            while let Ok(data) = recvr.recv().await {
+                let mut cur = Cursor::new(&data);
+                let channel = String::decode(&mut cur).unwrap();
+                let timestamp = chatview::Timestamp::decode(&mut cur).unwrap();
+                let id = chatview::MessageId::decode(&mut cur).unwrap();
+                let nick = String::decode(&mut cur).unwrap();
+                let msg = String::decode(&mut cur).unwrap();
+
+                let node_path = format!("/window/{channel}_chat_layer/content/chatty");
+                debug!(target: "app", "Attempting to relay message to {node_path}");
+                let Some(chatview) = sg_root2.clone().lookup_node(&node_path) else {
+                    warn!(target: "app", "Ignoring message since {node_path} doesn't exist");
+                    continue
+                };
+
+                // I prefer to just re-encode because the code is clearer.
+                let mut data = vec![];
+                timestamp.encode(&mut data).unwrap();
+                id.encode(&mut data).unwrap();
+                nick.encode(&mut data).unwrap();
+                msg.encode(&mut data).unwrap();
+                if let Err(err) = chatview.call_method("insert_line", data).await {
+                    error!(
+                        target: "app",
+                        "Call method {node_path}::insert_line({timestamp}, {id}, {nick}, '{msg}'): {err:?}"
+                    );
+                }
+            }
+        });
+        self.tasks.lock().unwrap().push(listen_recv);
+
+        plugin.link(darkirc);
+
+        debug!(target: "app", "Plugins loaded");
     }
     }
 
 
     /// Begins the draw of the tree, and then starts the UI procs.
     /// Begins the draw of the tree, and then starts the UI procs.
@@ -214,6 +263,14 @@ impl App {
             Pimpl::Window(win) => win.clone().start(self.event_pub.clone(), self.ex.clone()).await,
             Pimpl::Window(win) => win.clone().start(self.event_pub.clone(), self.ex.clone()).await,
             _ => panic!("wrong pimpl"),
             _ => panic!("wrong pimpl"),
         }
         }
+
+        let plugins = self.sg_root.clone().lookup_node("/plugin").unwrap();
+        for plugin in plugins.get_children() {
+            match &plugin.pimpl {
+                Pimpl::DarkIrc(darkirc) => darkirc.clone().start(self.ex.clone()).await,
+                _ => panic!("wrong pimpl"),
+            }
+        }
     }
     }
 
 
     /// Shutdown code here
     /// Shutdown code here

+ 33 - 1
bin/darkwallet/src/app/node.rs

@@ -520,7 +520,7 @@ pub fn create_chatview(name: &str) -> SceneNode {
 
 
 pub fn create_emoji_picker(name: &str) -> SceneNode {
 pub fn create_emoji_picker(name: &str) -> SceneNode {
     debug!(target: "app", "create_emoji_picker({name})");
     debug!(target: "app", "create_emoji_picker({name})");
-    let mut node = SceneNode::new(name, SceneNodeType::Image);
+    let mut node = SceneNode::new(name, SceneNodeType::EmojiPicker);
 
 
     let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
     let mut prop = Property::new("rect", PropertyType::Float32, PropertySubType::Pixel);
     prop.set_array_len(4);
     prop.set_array_len(4);
@@ -554,3 +554,35 @@ pub fn create_emoji_picker(name: &str) -> SceneNode {
 
 
     node
     node
 }
 }
+
+pub fn create_darkirc(name: &str) -> SceneNode {
+    debug!(target: "app", "create_darkirc({name})");
+    let mut node = SceneNode::new(name, SceneNodeType::Plugin);
+
+    let mut prop = Property::new("nick", PropertyType::Str, PropertySubType::Null);
+    prop.set_ui_text("Nick", "Nickname");
+    prop.set_defaults_str(vec!["anon".to_string()]).unwrap();
+    node.add_property(prop).unwrap();
+
+    node.add_signal(
+        "recv",
+        "Message received",
+        vec![
+            ("channel", "Channel", CallArgType::Str),
+            ("timestamp", "Timestamp", CallArgType::Uint64),
+            ("id", "ID", CallArgType::Hash),
+            ("nick", "Nick", CallArgType::Str),
+            ("msg", "Message", CallArgType::Str),
+        ],
+    )
+    .unwrap();
+
+    node.add_method(
+        "send",
+        vec![("channel", "Channel", CallArgType::Str), ("msg", "Message", CallArgType::Str)],
+        None,
+    )
+    .unwrap();
+
+    node
+}

+ 19 - 3
bin/darkwallet/src/app/schema/chat.rs

@@ -536,9 +536,9 @@ pub async fn make(
 
 
     let tree_name = channel.to_string() + "__chat_tree";
     let tree_name = channel.to_string() + "__chat_tree";
     let chat_tree = db.open_tree(tree_name.as_bytes()).unwrap();
     let chat_tree = db.open_tree(tree_name.as_bytes()).unwrap();
-    if chat_tree.is_empty() {
-        populate_tree(&chat_tree);
-    }
+    //if chat_tree.is_empty() {
+    //    populate_tree(&chat_tree);
+    //}
     debug!(target: "app", "db has {} lines", chat_tree.len());
     debug!(target: "app", "db has {} lines", chat_tree.len());
     let node = node
     let node = node
         .setup(|me| {
         .setup(|me| {
@@ -882,11 +882,27 @@ pub async fn make(
     node.register("click", slot).unwrap();
     node.register("click", slot).unwrap();
     let editz_text2 = editz_text.clone();
     let editz_text2 = editz_text.clone();
     let channel2 = channel.to_string();
     let channel2 = channel.to_string();
+    let sg_root2 = app.sg_root.clone();
     let listen_click = app.ex.spawn(async move {
     let listen_click = app.ex.spawn(async move {
+        let channel = format!("#{channel2}");
         while let Ok(_) = recvr.recv().await {
         while let Ok(_) = recvr.recv().await {
             let text = editz_text2.get();
             let text = editz_text2.get();
             info!(target: "app::chat", "Send '{text}' to channel: #{channel2}");
             info!(target: "app::chat", "Send '{text}' to channel: #{channel2}");
             editz_text2.set("");
             editz_text2.set("");
+
+            let darkirc = sg_root2.clone().lookup_node("/plugin/darkirc").unwrap();
+
+            if text.starts_with("/nick") {
+                let nick = text.split_whitespace().nth(1).unwrap_or("anon");
+                info!(target: "app::chat", "Setting nick to: {nick}");
+                darkirc.set_property_str(Role::App, "nick", nick);
+                continue
+            }
+
+            let mut data = vec![];
+            channel.encode(&mut data).unwrap();
+            text.encode(&mut data).unwrap();
+            darkirc.call_method("send", data).await.unwrap();
         }
         }
     });
     });
     app.tasks.lock().unwrap().push(listen_click);
     app.tasks.lock().unwrap().push(listen_click);

+ 13 - 9
bin/darkwallet/src/app/schema/mod.rs

@@ -53,23 +53,26 @@ mod android_ui_consts {
 
 
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
 mod ui_consts {
 mod ui_consts {
-    pub const CHATDB_PATH: &str = "APPDATA/chatdb/";
+    use std::path::PathBuf;
+
     pub const BG_PATH: &str = "bg.png";
     pub const BG_PATH: &str = "bg.png";
     pub use super::android_ui_consts::*;
     pub use super::android_ui_consts::*;
 
 
-    pub fn get_chatdb_path() -> String {
-        CHATDB_PATH.replace("APPDATA", &crate::android::get_appdata_path())
+    pub fn get_chatdb_path() -> PathBuf {
+        use crate::android::get_appdata_path;
+        get_appdata_path().join("chatdb")
     }
     }
 }
 }
 
 
 #[cfg(feature = "emulate-android")]
 #[cfg(feature = "emulate-android")]
 mod ui_consts {
 mod ui_consts {
-    pub const CHATDB_PATH: &str = "chatdb";
+    use std::path::PathBuf;
+
     pub const BG_PATH: &str = "assets/bg.png";
     pub const BG_PATH: &str = "assets/bg.png";
     pub use super::android_ui_consts::*;
     pub use super::android_ui_consts::*;
 
 
-    pub fn get_chatdb_path() -> String {
-        CHATDB_PATH.to_string()
+    pub fn get_chatdb_path() -> PathBuf {
+        dirs::cache_dir().unwrap().join("darkfi/chatdb")
     }
     }
 }
 }
 
 
@@ -78,12 +81,13 @@ mod ui_consts {
     not(feature = "emulate-android")
     not(feature = "emulate-android")
 ))]
 ))]
 mod ui_consts {
 mod ui_consts {
-    pub const CHATDB_PATH: &str = "chatdb";
+    use std::path::PathBuf;
+
     pub const BG_PATH: &str = "assets/bg.png";
     pub const BG_PATH: &str = "assets/bg.png";
     pub const EMOJI_PICKER_ICON_SIZE: f32 = 40.;
     pub const EMOJI_PICKER_ICON_SIZE: f32 = 40.;
 
 
-    pub fn get_chatdb_path() -> String {
-        CHATDB_PATH.to_string()
+    pub fn get_chatdb_path() -> PathBuf {
+        dirs::cache_dir().unwrap().join("darkfi/chatdb")
     }
     }
 }
 }
 
 

+ 25 - 54
bin/darkwallet/src/darkirc.rs

@@ -45,9 +45,14 @@ use crate::{
 };
 };
 
 
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
-const EVGRDB_PATH: &str = "/data/data/darkfi.darkwallet/evgr/";
+fn get_evgrdb_path() -> String {
+    use crate::android::get_appdata_path;
+    get_appdata_path() + "/evgr/"
+}
 #[cfg(not(target_os = "android"))]
 #[cfg(not(target_os = "android"))]
-const EVGRDB_PATH: &str = "~/.local/darkfi/darkwallet/evgr/";
+fn get_evgrdb_path() -> String {
+    "~/.local/darkfi/darkwallet/evgr/".to_string()
+}
 
 
 const CHANNEL: &str = "#random";
 const CHANNEL: &str = "#random";
 
 
@@ -83,21 +88,20 @@ pub struct DarkIrcBackend {
     ex: ExecutorPtr,
     ex: ExecutorPtr,
     p2p: P2pPtr,
     p2p: P2pPtr,
     event_graph: EventGraphPtr,
     event_graph: EventGraphPtr,
-    tasks: SyncMutex<Vec<smol::Task<()>>>,
     db: sled::Db,
     db: sled::Db,
 
 
-    chatview_node: SceneNodePtr,
-    sendbtn_node: SceneNodePtr,
-    editbox_node: SceneNodePtr,
-    editbox_text: PropertyStr,
-    chatview_scroll: PropertyFloat32,
-    upgrade_popup_is_visible: PropertyBool,
-
+    //chatview_node: SceneNodePtr,
+    //sendbtn_node: SceneNodePtr,
+    //editbox_node: SceneNodePtr,
+    //editbox_text: PropertyStr,
+    //chatview_scroll: PropertyFloat32,
+    //upgrade_popup_is_visible: PropertyBool,
     seen_msgs: SyncMutex<Vec<MessageId>>,
     seen_msgs: SyncMutex<Vec<MessageId>>,
 }
 }
 
 
 impl DarkIrcBackend {
 impl DarkIrcBackend {
     pub async fn new(sg_root: SceneNodePtr, ex: ExecutorPtr) -> darkfi::Result<Arc<Self>> {
     pub async fn new(sg_root: SceneNodePtr, ex: ExecutorPtr) -> darkfi::Result<Arc<Self>> {
+        /*
         let chatview_node = sg_root.clone().lookup_node("/window/view/chatty").unwrap();
         let chatview_node = sg_root.clone().lookup_node("/window/view/chatty").unwrap();
         let sendbtn_node = sg_root.clone().lookup_node("/window/view/send_btn").unwrap();
         let sendbtn_node = sg_root.clone().lookup_node("/window/view/send_btn").unwrap();
 
 
@@ -110,9 +114,10 @@ impl DarkIrcBackend {
         let upgrade_popup_node = sg_root.clone().lookup_node("/window/view/upgrade_popup").unwrap();
         let upgrade_popup_node = sg_root.clone().lookup_node("/window/view/upgrade_popup").unwrap();
         let upgrade_popup_is_visible =
         let upgrade_popup_is_visible =
             PropertyBool::wrap(&upgrade_popup_node, Role::App, "is_visible", 0).unwrap();
             PropertyBool::wrap(&upgrade_popup_node, Role::App, "is_visible", 0).unwrap();
+        */
 
 
         info!(target: "darkirc", "Starting DarkIRC backend");
         info!(target: "darkirc", "Starting DarkIRC backend");
-        let db = sled::open(EVGRDB_PATH)?;
+        let db = sled::open(get_evgrdb_path())?;
 
 
         let mut p2p_settings: NetSettings = Default::default();
         let mut p2p_settings: NetSettings = Default::default();
         p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
         p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
@@ -135,16 +140,14 @@ impl DarkIrcBackend {
             ex,
             ex,
             p2p,
             p2p,
             event_graph,
             event_graph,
-            tasks: SyncMutex::new(vec![]),
             db,
             db,
 
 
-            chatview_node,
-            sendbtn_node,
-            editbox_node,
-            editbox_text,
-            chatview_scroll,
-            upgrade_popup_is_visible,
-
+            //chatview_node,
+            //sendbtn_node,
+            //editbox_node,
+            //editbox_text,
+            //chatview_scroll,
+            //upgrade_popup_is_visible,
             seen_msgs: SyncMutex::new(vec![]),
             seen_msgs: SyncMutex::new(vec![]),
         }))
         }))
     }
     }
@@ -168,40 +171,6 @@ impl DarkIrcBackend {
         info!(target: "darkirc", "Starting P2P network");
         info!(target: "darkirc", "Starting P2P network");
         self.p2p.clone().start().await?;
         self.p2p.clone().start().await?;
 
 
-        // Connect the UI send up
-
-        let (slot, recvr) = Slot::new("send_button_clicked");
-        self.sendbtn_node.register("click", slot).unwrap();
-        let me = Arc::downgrade(&self);
-        let send_task = ex.spawn(async move {
-            while let Some(self_) = me.upgrade() {
-                let Ok(_) = recvr.recv().await else {
-                    error!(target: "ui::win", "Button click recvr closed");
-                    break
-                };
-                self_.handle_send().await;
-            }
-        });
-
-        let (slot, recvr) = Slot::new("enter_pressed");
-        self.editbox_node.register("enter_pressed", slot).unwrap();
-        let me = Arc::downgrade(&self);
-        let enter_task = ex.spawn(async move {
-            while let Some(self_) = me.upgrade() {
-                let Ok(_) = recvr.recv().await else {
-                    error!(target: "ui::win", "EditBox enter_pressed recvr closed");
-                    break
-                };
-                self_.handle_send().await;
-            }
-        });
-
-        {
-            let mut tasks = self.tasks.lock().unwrap();
-            assert!(tasks.is_empty());
-            *tasks = vec![send_task, enter_task];
-        }
-
         // Sync the DAG
         // Sync the DAG
 
 
         info!(target: "darkirc", "Waiting for some P2P connections...");
         info!(target: "darkirc", "Waiting for some P2P connections...");
@@ -297,10 +266,11 @@ impl DarkIrcBackend {
             privmsg.nick.encode(&mut arg_data).unwrap();
             privmsg.nick.encode(&mut arg_data).unwrap();
             privmsg.msg.encode(&mut arg_data).unwrap();
             privmsg.msg.encode(&mut arg_data).unwrap();
 
 
-            self.chatview_node.call_method("insert_line", arg_data).await.unwrap();
+            //self.chatview_node.call_method("insert_line", arg_data).await.unwrap();
         }
         }
     }
     }
 
 
+    /*
     async fn handle_send(&self) {
     async fn handle_send(&self) {
         // Get text from editbox
         // Get text from editbox
         let text = self.editbox_text.get();
         let text = self.editbox_text.get();
@@ -334,4 +304,5 @@ impl DarkIrcBackend {
 
 
         self.p2p.broadcast(&EventPut(event)).await;
         self.p2p.broadcast(&EventPut(event)).await;
     }
     }
+    */
 }
 }

+ 14 - 0
bin/darkwallet/src/error.rs

@@ -16,6 +16,8 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
  */
 
 
+use sled_overlay::sled;
+
 pub type Result<T> = std::result::Result<T, Error>;
 pub type Result<T> = std::result::Result<T, Error>;
 
 
 #[repr(u8)]
 #[repr(u8)]
@@ -116,4 +118,16 @@ pub enum Error {
 
 
     #[error("Unexpected token found")]
     #[error("Unexpected token found")]
     UnexpectedToken = 38,
     UnexpectedToken = 38,
+
+    #[error("Sled database error")]
+    SledDbErr = 39,
+
+    #[error("Service failed")]
+    ServiceFailed = 40,
+}
+
+impl From<sled::Error> for Error {
+    fn from(_: sled::Error) -> Error {
+        Error::SledDbErr
+    }
 }
 }

+ 10 - 6
bin/darkwallet/src/logger.rs

@@ -1,20 +1,25 @@
-use darkfi::util::path::expand_path;
 use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate};
 use file_rotate::{compression::Compression, suffix::AppendCount, ContentLimit, FileRotate};
 use log::{Level, LevelFilter, Log, Metadata, Record};
 use log::{Level, LevelFilter, Log, Metadata, Record};
 use simplelog::{
 use simplelog::{
     ColorChoice, CombinedLogger, Config, ConfigBuilder, SharedLogger, TermLogger, TerminalMode,
     ColorChoice, CombinedLogger, Config, ConfigBuilder, SharedLogger, TermLogger, TerminalMode,
     WriteLogger,
     WriteLogger,
 };
 };
-use std::{thread::sleep, time::Duration};
+use std::{path::PathBuf, thread::sleep, time::Duration};
 
 
 const LOGS_ENABLED: bool = true;
 const LOGS_ENABLED: bool = true;
 // Measured in bytes
 // Measured in bytes
 const LOGFILE_MAXSIZE: usize = 5_000_000;
 const LOGFILE_MAXSIZE: usize = 5_000_000;
 
 
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
-const LOGFILE_PATH: &str = "/sdcard/Download/darkwallet.log";
+fn logfile_path() -> PathBuf {
+    use crate::android::get_external_storage_path;
+    get_external_storage_path().join("Download/darkfi.log")
+}
+
 #[cfg(not(target_os = "android"))]
 #[cfg(not(target_os = "android"))]
-const LOGFILE_PATH: &str = "~/.local/darkfi/darkwallet.log";
+fn logfile_path() -> PathBuf {
+    dirs::cache_dir().unwrap().join("darkfi/darkfi.log")
+}
 
 
 #[cfg(target_os = "android")]
 #[cfg(target_os = "android")]
 mod android {
 mod android {
@@ -81,9 +86,8 @@ pub fn setup_logging() {
     let cfg = cfg.build();
     let cfg = cfg.build();
 
 
     if LOGS_ENABLED {
     if LOGS_ENABLED {
-        let logfile_path = expand_path(LOGFILE_PATH).unwrap();
         let log_file = FileRotate::new(
         let log_file = FileRotate::new(
-            logfile_path,
+            logfile_path(),
             AppendCount::new(0),
             AppendCount::new(0),
             ContentLimit::BytesSurpassed(LOGFILE_MAXSIZE),
             ContentLimit::BytesSurpassed(LOGFILE_MAXSIZE),
             Compression::None,
             Compression::None,

+ 3 - 23
bin/darkwallet/src/main.rs

@@ -53,15 +53,13 @@ use log::LevelFilter;
 mod android;
 mod android;
 mod app;
 mod app;
 mod build_info;
 mod build_info;
-mod darkirc;
-mod darkirc2;
 mod error;
 mod error;
 mod expr;
 mod expr;
 mod gfx;
 mod gfx;
 mod logger;
 mod logger;
 mod mesh;
 mod mesh;
 mod net;
 mod net;
-//mod plugin;
+mod plugin;
 mod prop;
 mod prop;
 mod pubsub;
 mod pubsub;
 //mod py;
 //mod py;
@@ -73,11 +71,7 @@ mod text;
 mod ui;
 mod ui;
 mod util;
 mod util;
 
 
-use crate::{
-    darkirc::{DarkIrcBackend, DarkIrcBackendPtr},
-    net::ZeroMQAdapter,
-    text::TextShaper,
-};
+use crate::{net::ZeroMQAdapter, text::TextShaper};
 
 
 pub type ExecutorPtr = Arc<smol::Executor<'static>>;
 pub type ExecutorPtr = Arc<smol::Executor<'static>>;
 
 
@@ -146,27 +140,13 @@ fn main() {
     let app2 = app.clone();
     let app2 = app.clone();
     let app_task = ex.spawn(async move {
     let app_task = ex.spawn(async move {
         app2.setup().await;
         app2.setup().await;
+        // Needed because accessing screen_size() is not allowed until window init
         cv_gfxwin_started2.wait().await;
         cv_gfxwin_started2.wait().await;
         app2.start().await;
         app2.start().await;
         cv_app_started2.notify();
         cv_app_started2.notify();
     });
     });
     async_runtime.push_task(app_task);
     async_runtime.push_task(app_task);
 
 
-    /*
-    let app2 = app.clone();
-    let sg_root = app.sg_root.clone();
-    let ex2 = ex.clone();
-    let darkirc_task = ex.spawn(async move {
-        cv_app_started.wait().await;
-        let darkirc_evgr = DarkIrcBackend::new(sg_root.clone(), ex2.clone()).await.unwrap();
-        *app2.darkirc_evgr.lock().unwrap() = Some(darkirc_evgr.clone());
-        if let Err(e) = darkirc_evgr.start(ex2).await {
-            error!("DarkIRC error: {e}")
-        }
-    });
-    async_runtime.push_task(darkirc_task);
-    */
-
     /*
     /*
     // Nice to see which events exist
     // Nice to see which events exist
     let ev_sub = event_pub.subscribe_key_down();
     let ev_sub = event_pub.subscribe_key_down();

+ 0 - 0
bin/darkwallet/src/plugin.rs → bin/darkwallet/src/plugin.old.rs


+ 371 - 0
bin/darkwallet/src/plugin/darkirc.rs

@@ -0,0 +1,371 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use async_trait::async_trait;
+use darkfi::{
+    event_graph::{
+        self,
+        proto::{EventPut, ProtocolEventGraph},
+        EventGraph, EventGraphPtr,
+    },
+    net::{session::SESSION_DEFAULT, settings::Settings as NetSettings, P2p, P2pPtr},
+    system::{sleep, Subscription},
+};
+use darkfi_serial::{
+    deserialize_async, serialize_async, AsyncEncodable, Decodable, Encodable, SerialDecodable,
+    SerialEncodable,
+};
+use sled_overlay::sled;
+use std::{
+    io::Cursor,
+    path::PathBuf,
+    sync::{Arc, Mutex as SyncMutex, OnceLock, Weak},
+    time::UNIX_EPOCH,
+};
+
+use crate::{
+    error::{Error, Result},
+    prop::{PropertyStr, Role},
+    scene::{MethodCallSub, Pimpl, SceneNodePtr, SceneNodeWeak},
+    ui::{chatview::MessageId, OnModify},
+    ExecutorPtr,
+};
+
+use super::PluginObject;
+
+#[cfg(target_os = "android")]
+fn get_evgrdb_path() -> PathBuf {
+    use crate::android::get_appdata_path;
+    get_appdata_path().join("evgr")
+}
+#[cfg(not(target_os = "android"))]
+fn get_evgrdb_path() -> PathBuf {
+    dirs::cache_dir().unwrap().join("darkfi/evgr")
+}
+
+#[cfg(target_os = "android")]
+fn nick_filename() -> PathBuf {
+    use crate::android::get_appdata_path;
+    get_appdata_path().join("/nick.txt")
+}
+#[cfg(not(target_os = "android"))]
+fn nick_filename() -> PathBuf {
+    dirs::cache_dir().unwrap().join("darkfi/nick.txt")
+}
+
+/// Due to drift between different machine's clocks, if the message timestamp is recent
+/// then we will just correct it to the current time so messages appear sequential in the UI.
+const RECENT_TIME_DIST: u64 = 10_000;
+
+macro_rules! d { ($($arg:tt)*) => { debug!(target: "plugin::darkirc", $($arg)*); } }
+macro_rules! inf { ($($arg:tt)*) => { info!(target: "plugin::darkirc", $($arg)*); } }
+macro_rules! err { ($($arg:tt)*) => { error!(target: "plugin::darkirc", $($arg)*); } }
+
+#[derive(Clone, Debug, SerialEncodable, SerialDecodable)]
+pub struct Privmsg {
+    pub channel: String,
+    pub nick: String,
+    pub msg: String,
+}
+
+impl Privmsg {
+    pub fn new(channel: String, nick: String, msg: String) -> Self {
+        Self { channel, nick, msg }
+    }
+
+    pub fn msg_id(&self, timest: u64) -> MessageId {
+        let mut hasher = blake3::Hasher::new();
+        timest.encode(&mut hasher).unwrap();
+        self.channel.encode(&mut hasher).unwrap();
+        self.nick.encode(&mut hasher).unwrap();
+        self.msg.encode(&mut hasher).unwrap();
+        MessageId(hasher.finalize().into())
+    }
+}
+
+pub type DarkIrcPtr = Arc<DarkIrc>;
+
+pub struct DarkIrc {
+    node: SceneNodeWeak,
+    tasks: OnceLock<Vec<smol::Task<()>>>,
+
+    p2p: P2pPtr,
+    event_graph: EventGraphPtr,
+    db: sled::Db,
+
+    seen_msgs: SyncMutex<Vec<MessageId>>,
+    nick: PropertyStr,
+}
+
+impl DarkIrc {
+    pub async fn new(node: SceneNodeWeak, ex: ExecutorPtr) -> Result<Pimpl> {
+        let node_ref = &node.upgrade().unwrap();
+        let nick = PropertyStr::wrap(node_ref, Role::Internal, "nick", 0).unwrap();
+
+        inf!("Starting DarkIRC backend");
+        let evgr_path = get_evgrdb_path();
+        let db = match sled::open(&evgr_path) {
+            Ok(db) => db,
+            Err(err) => {
+                err!("Sled database '{}' failed to open: {err}!", evgr_path.display());
+                return Err(Error::SledDbErr);
+            }
+        };
+
+        let mut p2p_settings: NetSettings = Default::default();
+        p2p_settings.app_version = semver::Version::parse("0.5.0").unwrap();
+        p2p_settings.seeds.push(url::Url::parse("tcp+tls://lilith1.dark.fi:5262").unwrap());
+
+        let p2p = match P2p::new(p2p_settings, ex.clone()).await {
+            Ok(p2p) => p2p,
+            Err(err) => {
+                err!("Create p2p network failed: {err}!");
+                return Err(Error::ServiceFailed);
+            }
+        };
+
+        let event_graph = match EventGraph::new(
+            p2p.clone(),
+            db.clone(),
+            std::path::PathBuf::new(),
+            false,
+            "darkirc_dag",
+            1,
+            ex.clone(),
+        )
+        .await
+        {
+            Ok(evgr) => evgr,
+            Err(err) => {
+                err!("Create event graph failed: {err}!");
+                return Err(Error::ServiceFailed);
+            }
+        };
+
+        if let Ok(prev_nick) = std::fs::read_to_string(nick_filename()) {
+            nick.set(prev_nick);
+        }
+
+        let self_ = Arc::new(Self {
+            node,
+            tasks: OnceLock::new(),
+
+            p2p,
+            event_graph,
+            db,
+
+            seen_msgs: SyncMutex::new(vec![]),
+            nick,
+        });
+        Ok(Pimpl::DarkIrc(self_))
+    }
+
+    async fn dag_sync(self: Arc<Self>) {
+        inf!("Starting p2p network");
+        // This usually means we cannot listen on the inbound ports
+        if let Err(err) = self.p2p.clone().start().await {
+            err!("Failed to start p2p network: {err}!");
+            return
+        }
+
+        inf!("Waiting for some P2P connections...");
+        sleep(5).await;
+
+        // We'll attempt to sync {sync_attempts} times
+        let sync_attempts = 4;
+        for i in 1..=sync_attempts {
+            inf!("Syncing event DAG (attempt #{})", i);
+            match self.event_graph.dag_sync().await {
+                Ok(()) => break,
+                Err(e) => {
+                    if i == sync_attempts {
+                        err!("Failed syncing DAG. Exiting.");
+                        self.p2p.stop().await;
+                        return
+                    } else {
+                        // TODO: Maybe at this point we should prune or something?
+                        // TODO: Or maybe just tell the user to delete the DAG from FS.
+                        err!("Failed syncing DAG ({}), retrying in {}s...", e, 4);
+                        sleep(4).await;
+                    }
+                }
+            }
+        }
+    }
+
+    async fn relay_events(self: Arc<Self>, ev_sub: Subscription<event_graph::Event>) {
+        loop {
+            let ev = ev_sub.receive().await;
+
+            // Try to deserialize the `Event`'s content into a `Privmsg`
+            let privmsg: Privmsg = match deserialize_async(ev.content()).await {
+                Ok(v) => v,
+                Err(e) => {
+                    err!("[IRC CLIENT] Failed deserializing incoming Privmsg event: {}", e);
+                    continue
+                }
+            };
+
+            inf!(
+                "Relaying channel={}, ev_id={:?}, ev={:?}, privmsg={:?}",
+                privmsg.channel,
+                ev.id(),
+                ev,
+                privmsg
+            );
+
+            let timest = ev.timestamp;
+            // This is a hack to make messages appear sequentially in the UI
+            let mut adj_timest = timest;
+            let now_timest = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+            if timest.abs_diff(now_timest) < RECENT_TIME_DIST {
+                d!("Applied timestamp correction: <{timest}> => <{now_timest}>");
+                adj_timest = now_timest;
+            }
+
+            let msg_id = privmsg.msg_id(timest);
+            {
+                let mut seen = self.seen_msgs.lock().unwrap();
+                if seen.contains(&msg_id) {
+                    warn!(target: "plugin::darkirc", "Skipping duplicate seen message: {msg_id}");
+                    continue
+                }
+                seen.push(msg_id.clone());
+            }
+
+            // Strip off starting #
+            let mut channel = privmsg.channel;
+            if channel.is_empty() {
+                warn!(target: "plugin::darkirc", "Received privmsg with empty channel!");
+                continue
+            }
+            if channel.chars().next().unwrap() != '#' {
+                warn!(target: "plugin::darkirc", "Skipping encrypted channel: {channel}");
+                continue
+            }
+            channel.remove(0);
+
+            let mut arg_data = vec![];
+            channel.encode(&mut arg_data).unwrap();
+            ev.timestamp.encode(&mut arg_data).unwrap();
+            ev.id().as_bytes().encode(&mut arg_data).unwrap();
+            privmsg.nick.encode(&mut arg_data).unwrap();
+            privmsg.msg.encode(&mut arg_data).unwrap();
+
+            let node = self.node.upgrade().unwrap();
+            node.trigger("recv", arg_data).await.unwrap();
+        }
+    }
+
+    async fn process_send(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
+        let Ok(method_call) = sub.receive().await else {
+            d!("Event relayer closed");
+            return false
+        };
+
+        d!("method called: send({method_call:?})");
+        assert!(method_call.send_res.is_none());
+
+        fn decode_data(data: &[u8]) -> std::io::Result<(String, String)> {
+            let mut cur = Cursor::new(&data);
+            let channel = String::decode(&mut cur)?;
+            let msg = String::decode(&mut cur)?;
+            Ok((channel, msg))
+        }
+
+        let Ok((channel, msg)) = decode_data(&method_call.data) else {
+            err!("send() method invalid arg data");
+            return true
+        };
+
+        let Some(self_) = me.upgrade() else {
+            // Should not happen
+            panic!("self destroyed before send_method_task was stopped!");
+        };
+
+        self_.handle_send(channel, msg).await;
+
+        true
+    }
+
+    async fn handle_send(&self, channel: String, msg: String) {
+        let nick = self.nick.get();
+
+        // Send text to channel
+        let timest = UNIX_EPOCH.elapsed().unwrap().as_millis() as u64;
+        d!("Sending privmsg: {timest} #{channel}: <{nick}> {msg}");
+        let msg = Privmsg::new(channel, nick, msg);
+
+        let mut arg_data = vec![];
+        timest.encode_async(&mut arg_data).await.unwrap();
+        msg.msg_id(timest).encode_async(&mut arg_data).await.unwrap();
+        msg.nick.encode_async(&mut arg_data).await.unwrap();
+        msg.msg.encode_async(&mut arg_data).await.unwrap();
+
+        // Broadcast the msg
+
+        let evgr = self.event_graph.clone();
+        let event = event_graph::Event::new(serialize_async(&msg).await, &evgr).await;
+        if let Err(e) = evgr.dag_insert(&[event.clone()]).await {
+            error!(target: "darkirc", "Failed inserting new event to DAG: {}", e);
+        }
+
+        self.p2p.broadcast(&EventPut(event)).await;
+    }
+}
+
+#[async_trait]
+impl PluginObject for DarkIrc {
+    async fn start(self: Arc<Self>, ex: ExecutorPtr) {
+        inf!("Registering EventGraph P2P protocol");
+        let event_graph_ = Arc::clone(&self.event_graph);
+        let registry = self.p2p.protocol_registry();
+        registry
+            .register(SESSION_DEFAULT, move |channel, _| {
+                let event_graph_ = event_graph_.clone();
+                async move { ProtocolEventGraph::init(event_graph_, channel).await.unwrap() }
+            })
+            .await;
+
+        let me = Arc::downgrade(&self);
+
+        let node = &self.node.upgrade().unwrap();
+        let node_name = node.name.clone();
+        let node_id = node.id;
+
+        let method_sub = node.subscribe_method_call("send").unwrap();
+        let me2 = me.clone();
+        let send_method_task =
+            ex.spawn(async move { while Self::process_send(&me2, &method_sub).await {} });
+
+        let mut on_modify = OnModify::new(ex.clone(), node_name, node_id, me.clone());
+        async fn save_nick(self_: Arc<DarkIrc>) {
+            let _ = std::fs::write(nick_filename(), self_.nick.get());
+        }
+        on_modify.when_change(self.nick.prop(), save_nick);
+
+        let ev_sub = self.event_graph.event_pub.clone().subscribe().await;
+        let ev_task = ex.spawn(self.clone().relay_events(ev_sub));
+
+        // Sync the DAG
+        let dag_task = ex.spawn(self.clone().dag_sync());
+
+        let mut tasks = vec![send_method_task, ev_task, dag_task];
+        self.tasks.set(tasks);
+    }
+}

+ 30 - 0
bin/darkwallet/src/plugin/mod.rs

@@ -0,0 +1,30 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2024 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use async_trait::async_trait;
+use std::sync::Arc;
+
+use crate::ExecutorPtr;
+
+mod darkirc;
+pub use darkirc::{DarkIrc, DarkIrcPtr};
+
+#[async_trait]
+pub trait PluginObject {
+    async fn start(self: Arc<Self>, ex: ExecutorPtr) {}
+}

+ 7 - 2
bin/darkwallet/src/scene.rs

@@ -31,6 +31,7 @@ use std::{
 
 
 use crate::{
 use crate::{
     error::{Error, Result},
     error::{Error, Result},
+    plugin,
     prop::{Property, PropertyPtr, Role},
     prop::{Property, PropertyPtr, Role},
     pubsub::{Publisher, PublisherPtr, Subscription},
     pubsub::{Publisher, PublisherPtr, Subscription},
     ui,
     ui,
@@ -106,13 +107,16 @@ pub enum SceneNodeType {
     Texture = 13,
     Texture = 13,
     Fonts = 10,
     Fonts = 10,
     Font = 11,
     Font = 11,
-    Plugins = 14,
-    Plugin = 15,
+    //Plugins = 14,
+    //Plugin = 15,
     ChatView = 16,
     ChatView = 16,
     EditBox = 17,
     EditBox = 17,
     ChatEdit = 18,
     ChatEdit = 18,
     Image = 19,
     Image = 19,
     Button = 20,
     Button = 20,
+    EmojiPicker = 21,
+    PluginRoot = 100,
+    Plugin = 101,
 }
 }
 
 
 pub struct SceneNode {
 pub struct SceneNode {
@@ -473,4 +477,5 @@ pub enum Pimpl {
     Image(ui::ImagePtr),
     Image(ui::ImagePtr),
     Button(ui::ButtonPtr),
     Button(ui::ButtonPtr),
     EmojiPicker(ui::EmojiPickerPtr),
     EmojiPicker(ui::EmojiPickerPtr),
+    DarkIrc(plugin::DarkIrcPtr),
 }
 }

+ 1 - 1
bin/darkwallet/src/ui/chatview/mod.rs

@@ -82,7 +82,7 @@ pub struct ChatMsg {
     pub text: String,
     pub text: String,
 }
 }
 
 
-type Timestamp = u64;
+pub type Timestamp = u64;
 
 
 #[derive(Clone, SerialEncodable, SerialDecodable, PartialEq)]
 #[derive(Clone, SerialEncodable, SerialDecodable, PartialEq)]
 pub struct MessageId(pub [u8; 32]);
 pub struct MessageId(pub [u8; 32]);