diff --git a/Cargo.lock b/Cargo.lock index 1b224a4..ddc27f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -567,6 +567,7 @@ version = "0.1.0" dependencies = [ "base64", "chrono", + "degeon_core", "futures", "iced", "iced_native", @@ -575,6 +576,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "degeon_core" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "futures", + "iced", + "ironforce", + "pyo3", + "serde", + "serde_json", +] + [[package]] name = "der" version = "0.4.4" @@ -1580,6 +1595,29 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "indoc" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47741a8bc60fb26eb8d6e0238bbb26d8575ff623fdc97b1a2c00c050b9684ed8" +dependencies = [ + "indoc-impl", + "proc-macro-hack", +] + +[[package]] +name = "indoc-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce046d161f000fffde5f432a0d034d0341dc152643b2598ed5bfce44c4f3a8f0" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", + "unindent", +] + [[package]] name = "inplace_it" version = "0.3.3" @@ -2180,6 +2218,25 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "paste" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ca20c77d80be666aef2b45486da86238fabe33e38306bd3118fe4af33fa880" +dependencies = [ + "paste-impl", + "proc-macro-hack", +] + +[[package]] +name = "paste-impl" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a7db200b97ef370c8e6de0088252f7e0dfff7d047a28528e47456c0fc98b6" +dependencies = [ + "proc-macro-hack", +] + [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -2290,6 +2347,12 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.32" @@ -2299,6 +2362,54 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "pyo3" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35100f9347670a566a67aa623369293703322bb9db77d99d7df7313b575ae0c8" +dependencies = [ + "cfg-if 1.0.0", + "indoc", + "libc", + "parking_lot", + "paste", + "pyo3-build-config", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d12961738cacbd7f91b7c43bc25cfeeaa2698ad07a04b3be0aa88b950865738f" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pyo3-macros" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0bc5215d704824dfddddc03f93cb572e1155c68b6761c37005e1c288808ea8" +dependencies = [ + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71623fc593224afaab918aa3afcaf86ed2f43d34f6afde7f3922608f253240df" +dependencies = [ + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.10" @@ -2938,6 +3049,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + [[package]] name = "url" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index 19b344e..c771f67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] -members = ["degeon"] +members = ["degeon", "degeon_core"] [features] default = [] diff --git a/degeon/Cargo.toml b/degeon/Cargo.toml index 84e822e..7a33055 100644 --- a/degeon/Cargo.toml +++ b/degeon/Cargo.toml @@ -7,7 +7,8 @@ edition = "2021" [dependencies] iced = { version = "0.3.0", features = ["glow"] } -ironforce = { path = "../", features = ["std"] } +ironforce = { path = "..", features = ["std"] } +degeon_core = { path = "../degeon_core" } base64 = "0.13.0" serde = { version = "1.0" } serde_json = "1.0.72" diff --git a/degeon/src/chat.rs b/degeon/src/chat.rs index 26dcac0..cc0973d 100644 --- a/degeon/src/chat.rs +++ b/degeon/src/chat.rs @@ -1,44 +1,41 @@ use iced::{Align, Button, Column, Element, Length, Row, Text, TextInput}; use iced_native::button; -use ironforce::{Keys, PublicKey}; use crate::gui_events::GuiEvent; -use crate::message::{DegMessage, DegMessageContent, Profile}; use crate::state; use crate::styles::style; -use serde::{Serialize, Deserialize}; +pub use degeon_core::Chat; +pub trait RenderableChat { + /// Render header of the chat + fn header<'a>(name: String) -> Element<'a, GuiEvent>; -/// A chat in the messenger -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Chat { - /// Public key of the other user - pub pkey: PublicKey, - /// Messages in this chat - pub messages: Vec, - /// Profile of the other user - pub profile: Profile, - /// Scroll position - pub scrolled: f32, - /// Message in the field - pub input: String, -} + /// Render the sending field + fn send_field<'a>( + input: String, + text_input_state: &'a mut iced::text_input::State, + send_button_state: &'a mut iced::button::State, + ) -> Element<'a, GuiEvent>; -impl Chat { - /// Create a new chat - pub fn new(pkey: PublicKey) -> Self { - Self { - pkey, - messages: vec![], - profile: Profile { name: "".to_string() }, - scrolled: 0.0, - input: "".to_string(), - } - } + /// Render chat preview + fn preview<'a>( + &'a self, + state: &'a mut button::State, + i: usize, + is_selected: bool, + ) -> Element<'a, GuiEvent>; + + /// Render the chat view + fn view<'a>( + &'a self, + text_input_state: &'a mut iced::text_input::State, + send_button_state: &'a mut iced::button::State, + ) -> Element<'a, GuiEvent>; } -impl Chat { + +impl RenderableChat for Chat { /// Render header of the chat - pub fn header<'a>(name: String) -> Element<'a, GuiEvent> { + fn header<'a>(name: String) -> Element<'a, GuiEvent> { iced::container::Container::new(Text::new(name.as_str()).color(iced::Color::WHITE)) .style(style::Container::Primary) .width(Length::Fill) @@ -48,7 +45,7 @@ impl Chat { } /// Render the sending field - pub fn send_field<'a>( + fn send_field<'a>( input: String, text_input_state: &'a mut iced::text_input::State, send_button_state: &'a mut iced::button::State, @@ -76,7 +73,7 @@ impl Chat { } /// Render chat preview - pub fn preview<'a>( + fn preview<'a>( &'a self, state: &'a mut button::State, i: usize, @@ -95,7 +92,7 @@ impl Chat { } /// Render the chat view - pub fn view<'a>( + fn view<'a>( &'a self, text_input_state: &'a mut iced::text_input::State, send_button_state: &'a mut iced::button::State, @@ -122,28 +119,4 @@ impl Chat { )) .into() } - - /// Create an example chat - pub fn example(i: usize, my_keys: &Keys) -> Chat { - let pkey = Keys::generate().get_public(); - Self { - messages: vec![ - DegMessage { - sender: pkey.clone(), - timestamp: chrono::Utc::now().timestamp(), - content: DegMessageContent::Text(format!("Example message {}", 2 * i)) - }, - DegMessage { - sender: my_keys.get_public(), - timestamp: chrono::Utc::now().timestamp(), - content: DegMessageContent::Text(format!("Example message {}", 2 * i + 1)) - }, - - ], - profile: Profile { name: format!("Example user ({})", i) }, - scrolled: 0.0, - pkey, - input: "".to_string(), - } - } } diff --git a/degeon/src/degeon_worker.rs b/degeon/src/degeon_worker.rs index 26c7652..3b9b4a8 100644 --- a/degeon/src/degeon_worker.rs +++ b/degeon/src/degeon_worker.rs @@ -1,286 +1,17 @@ -use crate::chat::Chat; use crate::gui_events::GuiEvent; -use crate::message::{Profile, ProtocolMsg}; -use futures::Stream; -use ironforce::res::IFResult; -use ironforce::{IronForce, Keys, Message, MessageType, PublicKey}; -use std::pin::Pin; -use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; -use serde::{Serialize, Deserialize}; +pub use degeon_core::{Degeon, DegeonData}; -/// The container for logic, data, IF and protocol interactions -#[derive(Clone)] -pub struct Degeon { - /// The list of all chats for this instance - pub chats: Vec, - /// Profile of this user - pub profile: Profile, - /// Keys of this user - pub keys: Keys, - /// The IF worker - pub ironforce: Arc>, +pub(crate) struct DegeonContainer { + inner: Degeon, } -/// Data for serialization -#[derive(Serialize, Deserialize)] -pub struct DegeonData { - pub chats: Vec, - pub profile: Profile, - pub keys: Keys, -} - -/// Load IF and launch the main loop -/// -/// Returns ironforce and keys -fn get_initialized_ironforce() -> (Arc>, Keys) { - let ironforce = IronForce::from_file("".to_string()).unwrap(); - let keys = ironforce.keys.clone(); - println!("ID: {}", keys.get_public().get_short_id()); - let (_thread, ironforce) = ironforce.launch_main_loop(1000); - (ironforce, keys) -} - -impl Default for Degeon { - fn default() -> Self { - let (ironforce, keys) = get_initialized_ironforce(); - Self { - chats: vec![], - profile: Profile::default(), - keys, - ironforce, - } - } -} - -impl Degeon { - /// Get profile for the current user - pub fn get_profile(&self) -> Profile { - self.profile.clone() - } - - /// Find a chat in the list for a given public key - pub fn chat_with(&self, pkey: &PublicKey) -> Option { - self.chats.iter().position(|chat| &chat.pkey == pkey) - } - - /// Process the incoming message and act accordingly - pub fn process_message(&self, msg: ironforce::Message) -> IFResult> { - let deg_msg: ProtocolMsg = - match serde_json::from_slice(msg.get_decrypted(&self.keys)?.as_slice()) { - Ok(r) => r, - Err(_) => return Ok(None), - }; - let sender = msg.get_sender(&self.keys).unwrap(); - println!( - "check_rec: {:?}, sender==self: {:?}", - msg.check_recipient(&self.keys), - sender == self.keys.get_public() - ); - if !msg.check_recipient(&self.keys) || sender == self.keys.get_public() { - return Ok(None); - } - println!("{:?}", deg_msg); - Ok(match °_msg { - ProtocolMsg::NewMessage(deg_msg) => { - Some(GuiEvent::NewMessageInChat(sender, deg_msg.clone())) - } - ProtocolMsg::ProfileRequest | ProtocolMsg::Ping => { - Some(GuiEvent::WeHaveToSendProfile(sender)) - } - ProtocolMsg::ProfileResponse(prof) => Some(GuiEvent::SetProfile(sender, prof.clone())), - }) - } - - /// Send a multicast message through the network - pub fn send_multicast(&self, msg: ProtocolMsg) -> IFResult<()> { - self.ironforce.lock().unwrap().send_to_all( - Message::build() - .message_type(MessageType::Broadcast) - .content(serde_json::to_vec(&msg)?) - .sign(&self.keys) - .build()?, - ) - } - - /// Send a message to a target through the network - pub fn send_message(&self, msg: ProtocolMsg, target: &PublicKey) -> IFResult<()> { - // if self.ironforce.lock().unwrap().get_tunnel(target).is_none() { - // println!("Creating a tunnel"); - // self.ironforce - // .lock() - // .unwrap() - // .initialize_tunnel_creation(target)?; - // let mut counter = 0; - // while self.ironforce.lock().unwrap().get_tunnel(target).is_none() { - // std::thread::sleep(std::time::Duration::from_millis(350)); - // counter += 1; - // if counter > 100 { - // return Err(IFError::TunnelNotFound); - // } - // } - // } - self.ironforce.lock().unwrap().send_to_all( - Message::build() - .message_type(MessageType::Broadcast) - .content(serde_json::to_vec(&msg)?) - .recipient(target) - .sign(&self.keys) - .build()?, - ) - } - - /// Created an iced command that sends a message to a target - pub fn get_send_command( - &self, - msg: ProtocolMsg, - target: &PublicKey, - ) -> iced::Command { - let if_clone = self.ironforce.clone(); - let _target = target.clone(); - let keys = self.keys.clone(); - - println!("Creating a send command: {:?}", msg); - iced::Command::perform(async {}, move |_| { - println!("Sending message: {:?}", msg); - if_clone - .lock() - .unwrap() - .send_to_all( - Message::build() - .message_type(MessageType::Broadcast) - .content(serde_json::to_vec(&msg).unwrap()) - // todo: - // .recipient(&target) - .sign(&keys) - .build() - .unwrap(), - ) - .unwrap(); - GuiEvent::None - }) - } - - /// Create an iced command that sends a message through the network to a target - #[allow(dead_code)] - pub fn get_send_multicast_command(&self, msg: ProtocolMsg) -> iced::Command { - let keys = self.keys.clone(); - let if_clone = self.ironforce.clone(); - println!("Created a send command"); - iced::Command::perform( - async move { - println!("Sending message: {:?}", msg); - if_clone - .lock() - .unwrap() - .send_to_all( - Message::build() - .message_type(MessageType::Broadcast) - .content(serde_json::to_vec(&msg).unwrap()) - .sign(&keys) - .build() - .unwrap(), - ) - .unwrap() - }, - |_| GuiEvent::None, - ) - } -} - -const DEFAULT_FILENAME: &str = ".degeon.json"; - -impl Degeon { - /// Store most of the necessary data to string - pub fn serialize_to_string(&self) -> serde_json::Result { - let data = DegeonData { - chats: self.chats.clone(), - profile: self.get_profile(), - keys: self.keys.clone(), - }; - serde_json::to_string(&data) - } - - /// Restore `Degeon` from serialized data - pub fn restore_from_string(data: String) -> IFResult { - let data_res: serde_json::Result = serde_json::from_str(data.as_str()); - let data = match data_res { - Ok(r) => r, - Err(_) => return Ok(Self::default()), - }; - let (ironforce, _keys) = get_initialized_ironforce(); - ironforce.lock().unwrap().keys = data.keys.clone(); - let deg = Degeon { - chats: data.chats, - profile: data.profile, - keys: data.keys, - ironforce - }; - Ok(deg) - } - - /// Save to a file. If no filename is provided, the default is used - pub fn save_to_file(&self, filename: String) -> IFResult<()> { - let data = self.serialize_to_string()?; - let filename = if filename.is_empty() { - DEFAULT_FILENAME.to_string() - } else { - filename - }; - std::fs::write(filename, data)?; - Ok(()) - } - - /// Restore from a file. If no filename is provided, the default is used - pub fn restore_from_file(filename: String) -> IFResult { - let filename = if filename.is_empty() { - DEFAULT_FILENAME.to_string() - } else { - filename - }; - let content = std::fs::read_to_string(filename).unwrap_or_default(); - Self::restore_from_string(content) - } -} - -impl Stream for Degeon { - type Item = GuiEvent; - - fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - let timestamp_0 = std::time::Instant::now(); - let msg_raw = self.ironforce.lock().unwrap().read_message(); - let msg = msg_raw - .as_ref() - .map(|msg| self.process_message(msg.clone()).unwrap()); - if msg_raw.is_some() { - let msg_deg: ProtocolMsg = match serde_json::from_slice( - msg_raw - .as_ref() - .unwrap() - .get_decrypted(&self.keys) - .unwrap() - .as_slice(), - ) { - Ok(r) => r, - Err(_) => { - println!("Couldn't deserialize {:?}", msg_raw); - return Poll::Ready(Some(GuiEvent::None)); - } - }; - println!("{:?} -> {:?}", msg_deg, msg); - } - if timestamp_0.elapsed() < std::time::Duration::from_millis(5) { - std::thread::sleep(std::time::Duration::from_millis(5)); - } - match msg { - None => Poll::Ready(Some(GuiEvent::None)), - Some(None) => Poll::Ready(Some(GuiEvent::None)), - Some(Some(msg)) => Poll::Ready(Some(msg)), - } +impl DegeonContainer { + pub fn new(data: &Degeon) -> Self { + Self { inner: data.clone() } } } -impl iced_native::subscription::Recipe for Degeon +impl iced_native::subscription::Recipe for DegeonContainer where H: std::hash::Hasher, { @@ -290,7 +21,7 @@ where use std::hash::Hash; std::any::TypeId::of::().hash(state); - self.ironforce.lock().unwrap().hash(state); + self.inner.ironforce.lock().unwrap().hash(state); // std::time::SystemTime::now().hash(state); // std::time::UNIX_EPOCH @@ -304,6 +35,6 @@ where self: Box, _input: futures::stream::BoxStream<'static, I>, ) -> futures::stream::BoxStream<'static, Self::Output> { - Box::pin(self) + Box::pin(self.inner) } } diff --git a/degeon/src/gui_events.rs b/degeon/src/gui_events.rs index 6626307..1f91a93 100644 --- a/degeon/src/gui_events.rs +++ b/degeon/src/gui_events.rs @@ -1,26 +1 @@ -use crate::message::{DegMessage, Profile}; -use ironforce::PublicKey; -use crate::state::AppScreen; - -/// An enum with all possible events for this application -#[derive(Clone, Debug)] -pub enum GuiEvent { - /// Selection of a chat - ChatSelect(usize), - /// The user changed the value of "new message" field - Typed(String), - /// The user clicked "Send" - SendMessage, - /// A new messaged arrived - NewMessageInChat(PublicKey, DegMessage), - /// A profile response arrived and we should store it - SetProfile(PublicKey, Profile), - /// We should send profile (in response to profile request) - WeHaveToSendProfile(PublicKey), - /// Go to another screen - ChangeScreen(AppScreen), - /// Changed the name in the field on profile screen - ChangeName(String), - /// Nothing happened - None, -} +pub use degeon_core::{GuiEvent, AppScreen}; diff --git a/degeon/src/message.rs b/degeon/src/message.rs index a536bc6..a69e1cd 100644 --- a/degeon/src/message.rs +++ b/degeon/src/message.rs @@ -1,48 +1 @@ -use ironforce::PublicKey; -use serde::{Deserialize, Serialize}; - -/// A message in the messenger -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DegMessage { - pub sender: PublicKey, - pub timestamp: i64, - pub content: DegMessageContent, -} - -impl DegMessage { - /// Create a simple text message - pub fn new_text(text: String, my_key: &PublicKey) -> DegMessage { - Self { - sender: my_key.clone(), - timestamp: chrono::Utc::now().timestamp(), - content: DegMessageContent::Text(text), - } - } -} - -/// The content of the message -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum DegMessageContent { - Text(String), - File(Vec), - Service, -} - -/// User's profile -#[derive(Clone, Debug, Serialize, Deserialize, Default)] -pub struct Profile { - pub name: String, -} - -/// A protocol message (that's sent through IF) -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum ProtocolMsg { - /// Requesting profile - ProfileRequest, - /// Responding to the profile request with a profile - ProfileResponse(Profile), - /// A peer discovery - Ping, - /// A message is sent - NewMessage(DegMessage), -} +pub use degeon_core::{ProtocolMsg, Profile, DegMessage, DegMessageContent}; diff --git a/degeon/src/state.rs b/degeon/src/state.rs index 12919d3..4b5c32d 100644 --- a/degeon/src/state.rs +++ b/degeon/src/state.rs @@ -1,5 +1,5 @@ use crate::chat::Chat; -use crate::degeon_worker::Degeon; +use crate::degeon_worker::{Degeon, DegeonContainer}; use crate::gui_events::GuiEvent; use crate::message::{DegMessage, DegMessageContent, ProtocolMsg}; use crate::styles::style; @@ -9,6 +9,8 @@ use iced::{ Text, TextInput, VerticalAlignment, }; use ironforce::PublicKey; +use degeon_core::AppScreen; +use crate::chat::RenderableChat; /// Render a message into an iced `Element` pub fn view_message(msg: &DegMessage, pkey: PublicKey) -> Option> { @@ -39,23 +41,6 @@ pub fn view_message(msg: &DegMessage, pkey: PublicKey) -> Option Self { - AppScreen::Main - } -} /// The main application struct (for iced) #[derive(Default)] @@ -287,7 +272,7 @@ impl Application for DegeonApp { } fn subscription(&self) -> iced::Subscription { - iced::Subscription::from_recipe(self.data.clone()) + iced::Subscription::from_recipe(DegeonContainer::new(&self.data)) } fn view(&mut self) -> Element<'_, Self::Message> { diff --git a/degeon_core/Cargo.toml b/degeon_core/Cargo.toml new file mode 100644 index 0000000..c9430d5 --- /dev/null +++ b/degeon_core/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "degeon_core" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ironforce = { path = "..", features = ["std"] } +base64 = "0.13.0" +serde = { version = "1.0" } +serde_json = "1.0.72" +futures = "0.3.18" +chrono = "0.4.19" +iced = { version = "0.3.0", features = ["glow"] } + +[dependencies.pyo3] +version = "0.14.0" +features = ["extension-module"] + +[lib] +name = "degeon_core" +path = "src/lib.rs" +crate-type = ["cdylib", "rlib"] + diff --git a/degeon_core/src/chat.rs b/degeon_core/src/chat.rs new file mode 100644 index 0000000..d6f5f1f --- /dev/null +++ b/degeon_core/src/chat.rs @@ -0,0 +1,56 @@ +use ironforce::{Keys, PublicKey}; +use crate::message::{DegMessage, DegMessageContent, Profile}; +use serde::{Serialize, Deserialize}; + + +/// A chat in the messenger +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Chat { + /// Public key of the other user + pub pkey: PublicKey, + /// Messages in this chat + pub messages: Vec, + /// Profile of the other user + pub profile: Profile, + /// Scroll position + pub scrolled: f32, + /// Message in the field + pub input: String, +} + +impl Chat { + /// Create a new chat + pub fn new(pkey: PublicKey) -> Self { + Self { + pkey, + messages: vec![], + profile: Profile { name: "".to_string() }, + scrolled: 0.0, + input: "".to_string(), + } + } + + /// Create an example chat + pub fn example(i: usize, my_keys: &Keys) -> Chat { + let pkey = Keys::generate().get_public(); + Self { + messages: vec![ + DegMessage { + sender: pkey.clone(), + timestamp: chrono::Utc::now().timestamp(), + content: DegMessageContent::Text(format!("Example message {}", 2 * i)) + }, + DegMessage { + sender: my_keys.get_public(), + timestamp: chrono::Utc::now().timestamp(), + content: DegMessageContent::Text(format!("Example message {}", 2 * i + 1)) + }, + + ], + profile: Profile { name: format!("Example user ({})", i) }, + scrolled: 0.0, + pkey, + input: "".to_string(), + } + } +} diff --git a/degeon_core/src/degeon_worker.rs b/degeon_core/src/degeon_worker.rs new file mode 100644 index 0000000..53f7286 --- /dev/null +++ b/degeon_core/src/degeon_worker.rs @@ -0,0 +1,257 @@ +use crate::chat::Chat; +use crate::gui_events::GuiEvent; +use crate::message::{Profile, ProtocolMsg}; +use futures::Stream; +use ironforce::res::IFResult; +use ironforce::{IronForce, Keys, Message, MessageType, PublicKey}; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use pyo3::prelude::*; +use serde::{Serialize, Deserialize}; + +/// The container for logic, data, IF and protocol interactions +#[derive(Clone)] +#[pyclass] +pub struct Degeon { + /// The list of all chats for this instance + pub chats: Vec, + /// Profile of this user + pub profile: Profile, + /// Keys of this user + pub keys: Keys, + /// The IF worker + pub ironforce: Arc>, +} + +/// Data for serialization +#[derive(Serialize, Deserialize)] +pub struct DegeonData { + pub chats: Vec, + pub profile: Profile, + pub keys: Keys, +} + +/// Load IF and launch the main loop +/// +/// Returns ironforce and keys +fn get_initialized_ironforce() -> (Arc>, Keys) { + let ironforce = IronForce::from_file("".to_string()).unwrap(); + let keys = ironforce.keys.clone(); + println!("ID: {}", keys.get_public().get_short_id()); + let (_thread, ironforce) = ironforce.launch_main_loop(1000); + (ironforce, keys) +} + +impl Default for Degeon { + fn default() -> Self { + let (ironforce, keys) = get_initialized_ironforce(); + Self { + chats: vec![], + profile: Profile::default(), + keys, + ironforce, + } + } +} + +impl Degeon { + /// Get profile for the current user + pub fn get_profile(&self) -> Profile { + self.profile.clone() + } + + /// Find a chat in the list for a given public key + pub fn chat_with(&self, pkey: &PublicKey) -> Option { + self.chats.iter().position(|chat| &chat.pkey == pkey) + } + + /// Process the incoming message and act accordingly + pub fn process_message(&self, msg: ironforce::Message) -> IFResult> { + let deg_msg: ProtocolMsg = + match serde_json::from_slice(msg.get_decrypted(&self.keys)?.as_slice()) { + Ok(r) => r, + Err(_) => return Ok(None), + }; + let sender = msg.get_sender(&self.keys).unwrap(); + println!( + "check_rec: {:?}, sender==self: {:?}", + msg.check_recipient(&self.keys), + sender == self.keys.get_public() + ); + if !msg.check_recipient(&self.keys) || sender == self.keys.get_public() { + return Ok(None); + } + println!("{:?}", deg_msg); + Ok(match °_msg { + ProtocolMsg::NewMessage(deg_msg) => { + Some(GuiEvent::NewMessageInChat(sender, deg_msg.clone())) + } + ProtocolMsg::ProfileRequest | ProtocolMsg::Ping => { + Some(GuiEvent::WeHaveToSendProfile(sender)) + } + ProtocolMsg::ProfileResponse(prof) => Some(GuiEvent::SetProfile(sender, prof.clone())), + }) + } + + /// Send a multicast message through the network + pub fn send_multicast(&self, msg: ProtocolMsg) -> IFResult<()> { + self.ironforce.lock().unwrap().send_to_all( + Message::build() + .message_type(MessageType::Broadcast) + .content(serde_json::to_vec(&msg)?) + .sign(&self.keys) + .build()?, + ) + } + + /// Send a message to a target through the network + pub fn send_message(&self, msg: ProtocolMsg, target: &PublicKey) -> IFResult<()> { + // if self.ironforce.lock().unwrap().get_tunnel(target).is_none() { + // println!("Creating a tunnel"); + // self.ironforce + // .lock() + // .unwrap() + // .initialize_tunnel_creation(target)?; + // let mut counter = 0; + // while self.ironforce.lock().unwrap().get_tunnel(target).is_none() { + // std::thread::sleep(std::time::Duration::from_millis(350)); + // counter += 1; + // if counter > 100 { + // return Err(IFError::TunnelNotFound); + // } + // } + // } + self.ironforce.lock().unwrap().send_to_all( + Message::build() + .message_type(MessageType::Broadcast) + .content(serde_json::to_vec(&msg)?) + .recipient(target) + .sign(&self.keys) + .build()?, + ) + } + + /// Created an iced command that sends a message to a target + pub fn get_send_command( + &self, + msg: ProtocolMsg, + target: &PublicKey, + ) -> iced::Command { + let if_clone = self.ironforce.clone(); + let _target = target.clone(); + let keys = self.keys.clone(); + + println!("Creating a send command: {:?}", msg); + iced::Command::perform(async {}, move |_| { + println!("Sending message: {:?}", msg); + if_clone + .lock() + .unwrap() + .send_to_all( + Message::build() + .message_type(MessageType::Broadcast) + .content(serde_json::to_vec(&msg).unwrap()) + // todo: + // .recipient(&target) + .sign(&keys) + .build() + .unwrap(), + ) + .unwrap(); + GuiEvent::None + }) + } +} + +pub const DEFAULT_FILENAME: &str = ".degeon.json"; + +impl Degeon { + /// Store most of the necessary data to string + pub fn serialize_to_string(&self) -> serde_json::Result { + let data = DegeonData { + chats: self.chats.clone(), + profile: self.get_profile(), + keys: self.keys.clone(), + }; + serde_json::to_string(&data) + } + + /// Restore `Degeon` from serialized data + pub fn restore_from_string(data: String) -> IFResult { + let data_res: serde_json::Result = serde_json::from_str(data.as_str()); + let data = match data_res { + Ok(r) => r, + Err(_) => return Ok(Self::default()), + }; + let (ironforce, _keys) = get_initialized_ironforce(); + ironforce.lock().unwrap().keys = data.keys.clone(); + let deg = Degeon { + chats: data.chats, + profile: data.profile, + keys: data.keys, + ironforce + }; + Ok(deg) + } + + /// Save to a file. If no filename is provided, the default is used + pub fn save_to_file(&self, filename: String) -> IFResult<()> { + let data = self.serialize_to_string()?; + let filename = if filename.is_empty() { + DEFAULT_FILENAME.to_string() + } else { + filename + }; + std::fs::write(filename, data)?; + Ok(()) + } + + /// Restore from a file. If no filename is provided, the default is used + pub fn restore_from_file(filename: String) -> IFResult { + let filename = if filename.is_empty() { + DEFAULT_FILENAME.to_string() + } else { + filename + }; + let content = std::fs::read_to_string(filename).unwrap_or_default(); + Self::restore_from_string(content) + } +} + +impl Stream for Degeon { + type Item = GuiEvent; + + fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + let timestamp_0 = std::time::Instant::now(); + let msg_raw = self.ironforce.lock().unwrap().read_message(); + let msg = msg_raw + .as_ref() + .map(|msg| self.process_message(msg.clone()).unwrap()); + if msg_raw.is_some() { + let msg_deg: ProtocolMsg = match serde_json::from_slice( + msg_raw + .as_ref() + .unwrap() + .get_decrypted(&self.keys) + .unwrap() + .as_slice(), + ) { + Ok(r) => r, + Err(_) => { + println!("Couldn't deserialize {:?}", msg_raw); + return Poll::Ready(Some(GuiEvent::None)); + } + }; + println!("{:?} -> {:?}", msg_deg, msg); + } + if timestamp_0.elapsed() < std::time::Duration::from_millis(5) { + std::thread::sleep(std::time::Duration::from_millis(5)); + } + match msg { + None => Poll::Ready(Some(GuiEvent::None)), + Some(None) => Poll::Ready(Some(GuiEvent::None)), + Some(Some(msg)) => Poll::Ready(Some(msg)), + } + } +} \ No newline at end of file diff --git a/degeon_core/src/gui_events.rs b/degeon_core/src/gui_events.rs new file mode 100644 index 0000000..6ae9157 --- /dev/null +++ b/degeon_core/src/gui_events.rs @@ -0,0 +1,52 @@ +use crate::message::{DegMessage, Profile}; +use ironforce::PublicKey; +use serde::{Serialize, Deserialize}; + + +/// The screens (pages) of the app +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub enum AppScreen { + /// The one with the chats + Main, + /// Settings and profile + ProfileEditor, + /// The screen that appears if no peers are available and asks for the IP address of at least one peer + #[allow(dead_code)] + PeerInput, +} + + +impl Default for AppScreen { + fn default() -> Self { + AppScreen::Main + } +} + +/// An enum with all possible events for this application +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum GuiEvent { + /// Selection of a chat + ChatSelect(usize), + /// The user changed the value of "new message" field + Typed(String), + /// The user clicked "Send" + SendMessage, + /// A new messaged arrived + NewMessageInChat(PublicKey, DegMessage), + /// A profile response arrived and we should store it + SetProfile(PublicKey, Profile), + /// We should send profile (in response to profile request) + WeHaveToSendProfile(PublicKey), + /// Go to another screen + ChangeScreen(AppScreen), + /// Changed the name in the field on profile screen + ChangeName(String), + /// Nothing happened + None, +} + +impl GuiEvent { + pub fn get_json(&self) -> String { + serde_json::to_string(&self).unwrap() + } +} diff --git a/degeon_core/src/lib.rs b/degeon_core/src/lib.rs new file mode 100644 index 0000000..3d0e046 --- /dev/null +++ b/degeon_core/src/lib.rs @@ -0,0 +1,25 @@ +extern crate serde; +extern crate pyo3; +mod chat; +mod degeon_worker; +mod gui_events; +mod message; + +pub use chat::Chat; +pub use degeon_worker::{Degeon, DegeonData, DEFAULT_FILENAME}; +pub use message::{DegMessage, Profile, ProtocolMsg, DegMessageContent}; +pub use gui_events::{AppScreen, GuiEvent}; +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +#[pyfunction] +fn new_degeon() -> PyResult { + Ok(Degeon::default()) +} + +#[pymodule] +fn degeon_core(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_function(wrap_pyfunction!(new_degeon, m)?)?; + Ok(()) +} diff --git a/degeon_core/src/message.rs b/degeon_core/src/message.rs new file mode 100644 index 0000000..a536bc6 --- /dev/null +++ b/degeon_core/src/message.rs @@ -0,0 +1,48 @@ +use ironforce::PublicKey; +use serde::{Deserialize, Serialize}; + +/// A message in the messenger +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DegMessage { + pub sender: PublicKey, + pub timestamp: i64, + pub content: DegMessageContent, +} + +impl DegMessage { + /// Create a simple text message + pub fn new_text(text: String, my_key: &PublicKey) -> DegMessage { + Self { + sender: my_key.clone(), + timestamp: chrono::Utc::now().timestamp(), + content: DegMessageContent::Text(text), + } + } +} + +/// The content of the message +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum DegMessageContent { + Text(String), + File(Vec), + Service, +} + +/// User's profile +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct Profile { + pub name: String, +} + +/// A protocol message (that's sent through IF) +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum ProtocolMsg { + /// Requesting profile + ProfileRequest, + /// Responding to the profile request with a profile + ProfileResponse(Profile), + /// A peer discovery + Ping, + /// A message is sent + NewMessage(DegMessage), +}