From 84e1b5c87957b6a5bc408df0b497209b627ec547 Mon Sep 17 00:00:00 2001 From: ennucore Date: Wed, 15 Dec 2021 19:39:49 +0300 Subject: [PATCH] Documentation and serialization --- degeon/src/chat.rs | 16 +++- degeon/src/degeon_worker.rs | 128 ++++++++++++++++++++++++++++---- degeon/src/gui_events.rs | 5 +- degeon/src/main.rs | 5 -- degeon/src/message.rs | 11 ++- degeon/src/state.rs | 143 +++++++++++++++++++++++------------- degeon/src/styles.rs | 1 + src/interfaces/ip.rs | 6 +- src/ironforce.rs | 17 ++++- 9 files changed, 254 insertions(+), 78 deletions(-) diff --git a/degeon/src/chat.rs b/degeon/src/chat.rs index acff823..26dcac0 100644 --- a/degeon/src/chat.rs +++ b/degeon/src/chat.rs @@ -5,17 +5,26 @@ use crate::gui_events::GuiEvent; use crate::message::{DegMessage, DegMessageContent, Profile}; use crate::state; use crate::styles::style; +use serde::{Serialize, Deserialize}; -#[derive(Clone, Debug)] + +/// 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, @@ -28,6 +37,7 @@ impl Chat { } impl Chat { + /// Render header of the chat pub 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) @@ -37,6 +47,7 @@ impl Chat { .into() } + /// Render the sending field pub fn send_field<'a>( input: String, text_input_state: &'a mut iced::text_input::State, @@ -64,6 +75,7 @@ impl Chat { .into() } + /// Render chat preview pub fn preview<'a>( &'a self, state: &'a mut button::State, @@ -82,6 +94,7 @@ impl Chat { .into() } + /// Render the chat view pub fn view<'a>( &'a self, text_input_state: &'a mut iced::text_input::State, @@ -110,6 +123,7 @@ impl Chat { .into() } + /// Create an example chat pub fn example(i: usize, my_keys: &Keys) -> Chat { let pkey = Keys::generate().get_public(); Self { diff --git a/degeon/src/degeon_worker.rs b/degeon/src/degeon_worker.rs index 096f074..26c7652 100644 --- a/degeon/src/degeon_worker.rs +++ b/degeon/src/degeon_worker.rs @@ -7,24 +7,46 @@ 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}; +/// 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, - pub my_name: String, + /// 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 = 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); + let (ironforce, keys) = get_initialized_ironforce(); Self { chats: vec![], - my_name: "".to_string(), + profile: Profile::default(), keys, ironforce, } @@ -32,16 +54,17 @@ impl Default for Degeon { } impl Degeon { + /// Get profile for the current user pub fn get_profile(&self) -> Profile { - Profile { - name: self.my_name.clone(), - } + 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()) { @@ -49,7 +72,11 @@ impl Degeon { 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()); + 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); } @@ -65,6 +92,7 @@ impl Degeon { }) } + /// Send a multicast message through the network pub fn send_multicast(&self, msg: ProtocolMsg) -> IFResult<()> { self.ironforce.lock().unwrap().send_to_all( Message::build() @@ -75,6 +103,7 @@ impl Degeon { ) } + /// 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"); @@ -101,13 +130,14 @@ impl Degeon { ) } + /// 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 _target = target.clone(); let keys = self.keys.clone(); println!("Creating a send command: {:?}", msg); @@ -120,6 +150,7 @@ impl Degeon { Message::build() .message_type(MessageType::Broadcast) .content(serde_json::to_vec(&msg).unwrap()) + // todo: // .recipient(&target) .sign(&keys) .build() @@ -130,6 +161,8 @@ impl Degeon { }) } + /// 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(); @@ -155,17 +188,84 @@ impl Degeon { } } +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()); + 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()) { + 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))} + Err(_) => { + println!("Couldn't deserialize {:?}", msg_raw); + return Poll::Ready(Some(GuiEvent::None)); + } }; println!("{:?} -> {:?}", msg_deg, msg); } diff --git a/degeon/src/gui_events.rs b/degeon/src/gui_events.rs index 1642634..b4bbfd4 100644 --- a/degeon/src/gui_events.rs +++ b/degeon/src/gui_events.rs @@ -1,5 +1,6 @@ use crate::message::{DegMessage, Profile}; use ironforce::PublicKey; +use crate::state::AppScreen; /// An enum with all possible events for this application #[derive(Clone, Debug)] @@ -10,14 +11,14 @@ pub enum GuiEvent { Typed(String), /// The user clicked "Send" SendMessage, - /// New chat shall be created because we have an incoming request - NewChat(PublicKey), /// 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), /// Nothing happened None, } diff --git a/degeon/src/main.rs b/degeon/src/main.rs index 869cbd9..5fb6cf4 100644 --- a/degeon/src/main.rs +++ b/degeon/src/main.rs @@ -7,13 +7,8 @@ mod styles; mod degeon_worker; use iced::Application; -use ironforce::res::IFResult; -use ironforce::{IronForce, Message, MessageType, PublicKey}; use crate::state::DegeonApp; - fn main() -> Result<(), Box> { - // let ironforce = IronForce::from_file("".to_string()).unwrap(); - // let _if_keys = ironforce.keys.clone(); Ok(DegeonApp::run(iced::Settings::default())?) } diff --git a/degeon/src/message.rs b/degeon/src/message.rs index 15afad6..a536bc6 100644 --- a/degeon/src/message.rs +++ b/degeon/src/message.rs @@ -1,6 +1,7 @@ use ironforce::PublicKey; use serde::{Deserialize, Serialize}; +/// A message in the messenger #[derive(Clone, Debug, Serialize, Deserialize)] pub struct DegMessage { pub sender: PublicKey, @@ -9,6 +10,7 @@ pub struct DegMessage { } impl DegMessage { + /// Create a simple text message pub fn new_text(text: String, my_key: &PublicKey) -> DegMessage { Self { sender: my_key.clone(), @@ -18,6 +20,7 @@ impl DegMessage { } } +/// The content of the message #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DegMessageContent { Text(String), @@ -25,15 +28,21 @@ pub enum DegMessageContent { Service, } -#[derive(Clone, Debug, Serialize, Deserialize)] +/// 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), } diff --git a/degeon/src/state.rs b/degeon/src/state.rs index c86e93c..7787cfe 100644 --- a/degeon/src/state.rs +++ b/degeon/src/state.rs @@ -1,21 +1,18 @@ use crate::chat::Chat; use crate::degeon_worker::Degeon; use crate::gui_events::GuiEvent; -use crate::message::{DegMessage, DegMessageContent, Profile, ProtocolMsg}; +use crate::message::{DegMessage, DegMessageContent, ProtocolMsg}; use crate::styles::style; use core::default::Default; -use futures::Stream; +use iced::window::Mode; use iced::{ - button, Align, Application, Button, Column, Element, HorizontalAlignment, Length, Row, Text, - TextInput, VerticalAlignment, + button, Align, Application, Button, Color, Column, Element, HorizontalAlignment, Length, Row, + Settings, Text, TextInput, VerticalAlignment, }; use ironforce::res::{IFError, IFResult}; -use ironforce::{IronForce, Keys, Message, MessageType, PublicKey}; -use std::hash::Hash; -use std::pin::Pin; -use std::sync::{Arc, Mutex}; -use std::task::{Context, Poll}; +use ironforce::PublicKey; +/// Render a message into an iced `Element` pub fn view_message(msg: &DegMessage, pkey: PublicKey) -> Option> { let is_from_me = pkey != msg.sender; match &msg.content { @@ -44,18 +41,44 @@ pub fn view_message(msg: &DegMessage, pkey: PublicKey) -> Option Self { + AppScreen::Main + } +} + +/// The main application struct (for iced) #[derive(Default)] pub struct DegeonApp { + /// The container for logic, data and IF pub data: Degeon, + /// Current screen + screen: AppScreen, + /// Selected chat (on the main screen) selected_chat: usize, + /// Send button send_button_state: iced::button::State, + /// Message input field text_input_state: iced::text_input::State, + /// Buttons for chat previews preview_button_states: Vec, } impl DegeonApp { + /// Render the list of chats with all previews fn chat_list<'a>( - chats: &'a Vec, + chats: &'a [Chat], preview_button_states: &'a mut Vec, selected: usize, ) -> Element<'a, GuiEvent> { @@ -77,8 +100,9 @@ impl DegeonApp { .into() } + /// Render active chat section pub fn active_chat<'a>( - chats: &'a Vec, + chats: &'a [Chat], selected_chat: usize, send_button_state: &'a mut button::State, text_input_state: &'a mut iced::text_input::State, @@ -95,20 +119,52 @@ impl DegeonApp { } } +impl DegeonApp { + fn main_view(&mut self) -> Element { + let Self { + data: Degeon { chats, .. }, + selected_chat, + send_button_state, + text_input_state, + preview_button_states, + .. + } = self; + Row::new() + .padding(20) + .push(Self::chat_list( + chats, + preview_button_states, + *selected_chat, + )) + .push(Self::active_chat( + chats, + *selected_chat, + send_button_state, + text_input_state, + )) + .height(Length::Fill) + .into() + } +} + impl Application for DegeonApp { type Executor = iced::executor::Default; type Message = GuiEvent; type Flags = (); fn new(_: ()) -> (Self, iced::Command) { - let mut st = Self::default(); - st.data.chats = vec![ - Chat::example(1, &st.data.keys), - Chat::example(2, &st.data.keys), - ]; - st.preview_button_states = vec![Default::default(), Default::default()]; - st.data.my_name = "John".to_string(); - st.data.send_multicast(ProtocolMsg::Ping).unwrap(); + let mut data = Degeon::restore_from_file("".to_string()).unwrap(); + data.chats = vec![Chat::example(1, &data.keys), Chat::example(2, &data.keys)]; + data.profile.name = "John".to_string(); + data.send_multicast(ProtocolMsg::Ping).unwrap(); + let st = DegeonApp { + data, + screen: Default::default(), + selected_chat: 0, + send_button_state: Default::default(), + text_input_state: Default::default(), + preview_button_states: vec![Default::default(), Default::default()], + }; let data_clone = st.data.clone(); std::thread::spawn(move || { std::thread::sleep(std::time::Duration::from_secs(10)); @@ -142,19 +198,20 @@ impl Application for DegeonApp { .push(new_msg.clone()); let data_cloned = self.data.clone(); let target = self.data.chats[self.selected_chat].pkey.clone(); - std::thread::spawn(move || data_cloned.send_message(ProtocolMsg::NewMessage(new_msg), &target).unwrap()); - } - GuiEvent::NewChat(pkey) => { - if self.data.chat_with(&pkey).is_none() { - self.data.chats.push(Chat::new(pkey)) - } + std::thread::spawn(move || { + data_cloned + .send_message(ProtocolMsg::NewMessage(new_msg), &target) + .unwrap() + }); + self.data.save_to_file("".to_string()).unwrap(); } GuiEvent::NewMessageInChat(pkey, msg) => { if self.data.chat_with(&pkey).is_none() { self.data.chats.push(Chat::new(pkey.clone())) } let ind = self.data.chat_with(&pkey).unwrap(); - self.data.chats[ind].messages.push(msg) + self.data.chats[ind].messages.push(msg); + self.data.save_to_file("".to_string()).unwrap(); } GuiEvent::SetProfile(pkey, name) => { if self.data.chat_with(&pkey).is_none() { @@ -162,6 +219,7 @@ impl Application for DegeonApp { } let ind = self.data.chat_with(&pkey).unwrap(); self.data.chats[ind].profile = name; + self.data.save_to_file("".to_string()).unwrap(); } GuiEvent::None => {} GuiEvent::WeHaveToSendProfile(target) => { @@ -169,8 +227,9 @@ impl Application for DegeonApp { return self.data.get_send_command( ProtocolMsg::ProfileResponse(self.data.get_profile()), &target, - ) + ); } + GuiEvent::ChangeScreen(sc) => self.screen = sc, } iced::Command::none() } @@ -179,29 +238,11 @@ impl Application for DegeonApp { iced::Subscription::from_recipe(self.data.clone()) } - fn view(&mut self) -> Element { - let Self { - data: Degeon { chats, .. }, - selected_chat, - send_button_state, - text_input_state, - preview_button_states, - .. - } = self; - Row::new() - .padding(20) - .push(Self::chat_list( - chats, - preview_button_states, - *selected_chat, - )) - .push(Self::active_chat( - chats, - *selected_chat, - send_button_state, - text_input_state, - )) - .height(Length::Fill) - .into() + fn view(&mut self) -> Element<'_, Self::Message> { + match self.screen { + AppScreen::Main => self.main_view(), + AppScreen::ProfileEditor => todo!(), + AppScreen::PeerInput => todo!(), + } } } diff --git a/degeon/src/styles.rs b/degeon/src/styles.rs index 38e3635..fd8cc7e 100644 --- a/degeon/src/styles.rs +++ b/degeon/src/styles.rs @@ -6,6 +6,7 @@ pub mod style { pub enum Button { Primary, Secondary, + #[allow(dead_code)] Destructive, InactiveChat, } diff --git a/src/interfaces/ip.rs b/src/interfaces/ip.rs index 3ffc440..59e57ea 100644 --- a/src/interfaces/ip.rs +++ b/src/interfaces/ip.rs @@ -134,7 +134,7 @@ impl Interface for IPInterface { .iter() .find(|p| compare_addrs(p, connection_addr)) { - if let Some(Some(conn)) = IPInterface::new_connection(peer).ok() { + if let Ok(Some(conn)) = IPInterface::new_connection(peer) { new_connections.push(conn) } } @@ -276,7 +276,7 @@ impl Interface for IPInterface { println!("Error while sending: {:?}", e); e }, - ); + ).unwrap_or_default(); } } } @@ -401,7 +401,7 @@ impl IPInterface { .connections .iter() .filter_map(|conn| conn.peer_addr().ok()) - .position(|addr| compare_addrs(&peer, addr)) + .position(|addr| compare_addrs(peer, addr)) { self.connections.remove(ind); } diff --git a/src/ironforce.rs b/src/ironforce.rs index 15a3262..466ae39 100644 --- a/src/ironforce.rs +++ b/src/ironforce.rs @@ -16,7 +16,7 @@ const TUNNEL_MAX_REPEAT_COUNT: u32 = 3; #[cfg(feature = "std")] pub const DEFAULT_FILE: &str = ".if_data.json"; -/// Main worker +/// Main IF worker #[derive(Hash)] pub struct IronForce { /// Keys for this instance @@ -54,9 +54,13 @@ pub struct IronForce { /// Data for the serialization of IF #[derive(Serialize, Deserialize)] pub struct IFSerializationData { + /// Worker's keys pub keys: Keys, + /// Saved tunnels that go through this node pub tunnels: Vec, + /// Peers for transport pub peers: Vec, + /// Data for all interfaces (in IP, for example, that's port and IPs of peers) pub interfaces_data: Vec, } @@ -325,6 +329,7 @@ impl IronForce { self.messages.pop() } + /// Run one iteration of main loop: accepting incoming connections and messages, processing them pub fn main_loop_iteration(&mut self) -> IFResult<()> { self.transport.main_loop_iteration()?; while let Some((msg, inc_peer)) = self.transport.receive() { @@ -333,10 +338,12 @@ impl IronForce { Ok(()) } + /// Get an id for the public key of the worker fn short_id(&self) -> alloc::string::String { self.keys.get_public().get_short_id() } + /// Get `IFSerializationData` that can be stored in a file pub fn get_serialization_data(&self) -> IFSerializationData { IFSerializationData { keys: self.keys.clone(), @@ -346,6 +353,7 @@ impl IronForce { } } + /// Restore from `IFSerializationData` pub fn from_serialization_data(data: IFSerializationData) -> IFResult { Ok(Self { keys: data.keys, @@ -361,6 +369,9 @@ impl IronForce { }) } + /// Load from file (`filename`) with `IFSerializationData` + /// + /// If the filename is empty, the default filename is used #[cfg(feature = "std")] pub fn from_file(filename: alloc::string::String) -> IFResult { let filename = if filename.is_empty() { @@ -377,6 +388,9 @@ impl IronForce { } } + /// Save `IFSerializationData` to a file with `filename` + /// + /// If `filename` is None, the default filename is used #[cfg(feature = "std")] pub fn save_to_file(&self, filename: Option) -> IFResult<()> { std::fs::write( @@ -386,6 +400,7 @@ impl IronForce { Ok(()) } + /// Spawn a thread with IF main loop and return `Arc>` #[cfg(feature = "std")] pub fn launch_main_loop( mut self,