diff --git a/README.md b/README.md index bec9d83..b7fd1cd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,14 @@ IronForce is a peer-to-peer decentralized network, Degeon is a messenger built on it. -![](images/logo.png) +In this repository there is three Cargo crates and one python project: + +- `ironforce` is the decentralized network +- `degeon_core` contains messenger's protocol +- `degeon` is the messenger app in Rust + Iced +- `degeon_py` is a version of the messenger app in Python + Pygame + +![](images/dependency_graph.png) # Ironforce @@ -20,6 +27,49 @@ The Ironforce network: ![](images/scheme.png) +### Running IronForce + +The `IF` network has a worker daemon that can be used to read the broadcast messages and write them. + +Also, you can us `IF` as a Rust library (as it's done in Degeon). + +To get started: + +1. Clone this repository: `git clone ssh://git@gitlab.ennucore.com:10022/ironforce/ironforce.git` +2. Install Rust and Cargo. The best way to do this is to use RustUp: +```shell +sudo pacman -S rustup # this is for Arch Linux, on other distros use the corresponding package manager +rustup default nightly # this will install rust & cargo (nightly toolchain) +``` +3. Run the worker: +```shell +cd ironforce/ironforce +cargo build --features std # This will build ironforce network +# To run the worker, use this: +cargo run --bin worker --features std +``` + # Degeon -Degeon is just a messenger built on IronForce to show its abilities. \ No newline at end of file +Degeon is a messenger built on IronForce to show its abilities. +Its core is in the crate `degeon_core`. The crate can be used as a Python module or in Rust. + +To build it, install Cargo and Rust, then do this: +```shell +cd ironforce/degeon_core +cargo build +``` + +You will find the `libdegeon_core.so` file that can be used in Python in the `target/debug` directory. +It should be renamed to `degeon_core.so`. +In Windows Rust should generate a `.dll` file that you will be able to use likewise. + +There are two GUI options for this interface: +- A Rust + Iced client (preferred) +- A Pygame client + +To run the Rust client, `cd` into the `degeon` folder of this repository, then use cargo: +`cargo run`. +Note: this application requires Vulkan to be installed. +For me on Arch Linux and Intel GPU the installation command was `sudo pacman -S vulkan-intel`. +On most systems it should be pre-installed. \ No newline at end of file diff --git a/degeon/src/chat.rs b/degeon/src/chat.rs index 7b66382..d08532e 100644 --- a/degeon/src/chat.rs +++ b/degeon/src/chat.rs @@ -5,9 +5,10 @@ pub use degeon_core::Chat; use iced::{Align, Button, Column, Element, Length, Row, Text, TextInput}; use iced_native::button; +/// A chat that can be rendered in iced pub trait RenderableChat { /// Render header of the chat - fn header<'a>(name: String) -> Element<'a, GuiEvent>; + fn header<'a>(name: String, bio: String) -> Element<'a, GuiEvent>; /// Render the sending field fn send_field<'a>( @@ -35,8 +36,14 @@ pub trait RenderableChat { impl RenderableChat for Chat { /// Render header of the chat - fn header<'a>(name: String) -> Element<'a, GuiEvent> { - iced::container::Container::new(Text::new(name.as_str()).color(iced::Color::WHITE)) + /// + /// The header is a rectangle (container) with name and bio + fn header<'a>(name: String, bio: String) -> Element<'a, GuiEvent> { + iced::container::Container::new(Column::with_children( + vec![ + Text::new(name.as_str()).size(20).color(iced::Color::WHITE).into(), + Text::new(bio.as_str()).size(13).color(iced::Color::from_rgb(0.6, 0.6, 0.6)).into(), + ])) .style(style::Container::Primary) .width(Length::Fill) .height(Length::Units(50)) @@ -45,6 +52,9 @@ impl RenderableChat for Chat { } /// Render the sending field + /// + /// This field consists of a text input and a send button. + /// When the button is clicked, this element creates an event fn send_field<'a>( input: String, text_input_state: &'a mut iced::text_input::State, @@ -53,6 +63,7 @@ impl RenderableChat for Chat { Row::new() .width(Length::Fill) .padding(15) + // Text field .push( TextInput::new(text_input_state, "Message", input.as_str(), |st| { GuiEvent::Typed(st) @@ -60,6 +71,7 @@ impl RenderableChat for Chat { .padding(8) .width(Length::Fill), ) + // Send button .push( Button::new(send_button_state, Text::new("Send")) .on_press(GuiEvent::SendMessage) @@ -92,6 +104,8 @@ impl RenderableChat for Chat { } /// Render the chat view + /// + /// Chat view consists of a header, the messages and the input field fn view<'a>( &'a self, text_input_state: &'a mut iced::text_input::State, @@ -108,7 +122,9 @@ impl RenderableChat for Chat { .align_items(Align::End) .height(Length::Fill) .width(Length::FillPortion(4)) - .push(Self::header(self.profile.name.clone())) + // Header + .push(Self::header(self.profile.name.clone(), self.profile.bio.clone())) + // Messages (scrollable) .push( iced::Container::new( iced::Scrollable::new(scroll_state) @@ -125,6 +141,7 @@ impl RenderableChat for Chat { .height(Length::FillPortion(9)), ) .spacing(10) + // New message field .push(Self::send_field( self.input.to_string(), text_input_state, diff --git a/degeon/src/degeon_worker.rs b/degeon/src/degeon_worker.rs index 3b9b4a8..84cae5a 100644 --- a/degeon/src/degeon_worker.rs +++ b/degeon/src/degeon_worker.rs @@ -1,11 +1,13 @@ use crate::gui_events::GuiEvent; pub use degeon_core::{Degeon, DegeonData}; +/// A wrapper around `degeon_core`'s `Degeon` that can create a subscription for events pub(crate) struct DegeonContainer { inner: Degeon, } impl DegeonContainer { + /// Create a new container from inner data pub fn new(data: &Degeon) -> Self { Self { inner: data.clone() } } @@ -22,13 +24,6 @@ where std::any::TypeId::of::().hash(state); self.inner.ironforce.lock().unwrap().hash(state); - - // std::time::SystemTime::now().hash(state); - // std::time::UNIX_EPOCH - // .elapsed() - // .unwrap() - // .as_secs() - // .hash(state); } fn stream( diff --git a/degeon/src/gui_events.rs b/degeon/src/gui_events.rs index 1f91a93..cb7fa39 100644 --- a/degeon/src/gui_events.rs +++ b/degeon/src/gui_events.rs @@ -1 +1,2 @@ +// Just reusing events from `degeon_core` pub use degeon_core::{GuiEvent, AppScreen}; diff --git a/degeon/src/main.rs b/degeon/src/main.rs index 04b5dfd..ee79ce2 100644 --- a/degeon/src/main.rs +++ b/degeon/src/main.rs @@ -10,7 +10,9 @@ use iced::Application; use crate::state::DegeonApp; fn main() -> Result<(), Box> { + // Change default font let mut settings = iced::Settings::default(); settings.default_font = Some(include_bytes!("CRC35.otf")); + // Create and run the app Ok(DegeonApp::run(settings)?) } diff --git a/degeon/src/message.rs b/degeon/src/message.rs index a69e1cd..7d8d714 100644 --- a/degeon/src/message.rs +++ b/degeon/src/message.rs @@ -1 +1,2 @@ +/// Messages here are just from `degeon_core` pub use degeon_core::{ProtocolMsg, Profile, DegMessage, DegMessageContent}; diff --git a/degeon/src/profile_logo.png b/degeon/src/profile_logo.png new file mode 100644 index 0000000..d5db2f5 Binary files /dev/null and b/degeon/src/profile_logo.png differ diff --git a/degeon/src/state.rs b/degeon/src/state.rs index 4216ea3..fd3c6b0 100644 --- a/degeon/src/state.rs +++ b/degeon/src/state.rs @@ -13,6 +13,9 @@ use iced::{ use ironforce::PublicKey; /// Render a message into an iced `Element` +/// +/// A message is a rounded rectangle with text. +/// The background color of the rectangle depends on the sender of the message. pub fn view_message(msg: &DegMessage, pkey: PublicKey) -> Option> { let is_from_me = pkey != msg.sender; match &msg.content { @@ -128,6 +131,7 @@ impl DegeonApp { } impl DegeonApp { + /// Render the main screen with chat list and active chat on it fn main_view(&mut self) -> Element { let Self { data: Degeon { chats, .. }, @@ -158,6 +162,7 @@ impl DegeonApp { .into() } + /// Render the screen with profile settings fn profile_editor(&mut self) -> Element { let Self { data: @@ -232,10 +237,11 @@ impl Application for DegeonApp { type Message = GuiEvent; type Flags = (); + // Create a new application instance fn new(_: ()) -> (Self, iced::Command) { let mut data = Degeon::restore_from_file("".to_string()).unwrap(); if data.chats.is_empty() { - data.chats = vec![Chat::example(1, &data.keys), Chat::example(2, &data.keys)]; + data.chats = vec![Chat::example(0, &data.keys), Chat::example(1, &data.keys)]; } data.send_multicast(ProtocolMsg::Ping).unwrap(); let mut scroll: iced::scrollable::State = Default::default(); diff --git a/degeon/src/styles.rs b/degeon/src/styles.rs index 9f8aea7..929a5a4 100644 --- a/degeon/src/styles.rs +++ b/degeon/src/styles.rs @@ -2,6 +2,7 @@ pub mod style { use iced::container::Style; use iced::{button, Background, Color, Vector}; + /// The styles for buttons used in the app #[derive(Clone, Copy, PartialEq, Eq)] pub enum Button { Primary, @@ -32,6 +33,7 @@ pub mod style { } } + /// The styles for containers used in the app #[derive(Copy, Clone, PartialEq)] pub enum Container { Primary, diff --git a/degeon_core/src/chat.rs b/degeon_core/src/chat.rs index e2cde0c..da69b9d 100644 --- a/degeon_core/src/chat.rs +++ b/degeon_core/src/chat.rs @@ -1,10 +1,11 @@ -use ironforce::{Keys, PublicKey}; use crate::message::{DegMessage, DegMessageContent, Profile}; -use serde::{Serialize, Deserialize}; +use ironforce::{Keys, PublicKey}; use pyo3::prelude::*; - +use serde::{Deserialize, Serialize}; /// A chat in the messenger +/// +/// Contains user's profile and all the messages #[derive(Clone, Debug, Serialize, Deserialize)] #[pyclass] pub struct Chat { @@ -28,7 +29,10 @@ impl Chat { Self { pkey, messages: vec![], - profile: Profile { name: "".to_string(), bio: "".to_string() }, + profile: Profile { + name: "".to_string(), + bio: "".to_string(), + }, scrolled: 0.0, input: "".to_string(), } @@ -36,22 +40,47 @@ impl Chat { /// Create an example chat pub fn example(i: usize, my_keys: &Keys) -> Chat { + // A dummy key let pkey = Keys::generate().get_public(); - Self { - messages: vec![ - DegMessage { + let mut messages = vec![]; + if i == 0 { + for st in vec![ + "Welcome to Degeon!", + "It's a messenger app built on the IronForce decentralized network", + "Thanks to IronForce decentralized network, it's completely secure and anonymous", + "Your messages are encrypted and signed by you", + ] { + messages.push(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), bio: "".to_string() }, + timestamp: 0, + content: DegMessageContent::Text(st.to_string()), + }); + } + } else { + messages.push(DegMessage { + sender: pkey.clone(), + timestamp: 0, + content: DegMessageContent::Text( + "You can save information by sending messages in this chat:".to_string(), + ), + }); + messages.push(DegMessage { + sender: my_keys.get_public(), + timestamp: 0, + content: DegMessageContent::Text("Example saved message".to_string()), + }); + } + Self { + messages, + profile: Profile { + name: (if i == 0 { + "Degeon Info" + } else { + "Saved messages" + }) + .to_string(), + bio: "".to_string(), + }, scrolled: 0.0, pkey, input: "".to_string(), diff --git a/degeon_core/src/degeon_worker.rs b/degeon_core/src/degeon_worker.rs index 6fa091e..7418ffd 100644 --- a/degeon_core/src/degeon_worker.rs +++ b/degeon_core/src/degeon_worker.rs @@ -50,7 +50,7 @@ impl Default for Degeon { fn default() -> Self { let (ironforce, keys) = get_initialized_ironforce(); let st = Self { - chats: vec![], + chats: vec![Chat::example(0, &keys), Chat::example(1, &keys)], profile: Profile::default(), keys, ironforce, diff --git a/degeon_core/src/lib.rs b/degeon_core/src/lib.rs index 1a4848e..db80b54 100644 --- a/degeon_core/src/lib.rs +++ b/degeon_core/src/lib.rs @@ -17,11 +17,28 @@ fn new_degeon() -> PyResult { Ok(Degeon::restore_from_file("".to_string()).unwrap()) } +/// Check if there is stored Degeon profile here +#[pyfunction] +fn is_data_available() -> bool { + std::path::Path::new(DEFAULT_FILENAME).exists() +} + +/// Create a new profile with name +#[pyfunction] +fn new_degeon_with_name(name: String) -> PyResult { + let mut deg = Degeon::default(); + deg.profile.name = name; + Ok(deg) +} + +/// Define structs and functions that are exported into python #[pymodule] fn degeon_core(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_function(wrap_pyfunction!(new_degeon, m)?)?; + m.add_function(wrap_pyfunction!(is_data_available, m)?)?; + m.add_function(wrap_pyfunction!(new_degeon_with_name, m)?)?; Ok(()) } diff --git a/degeon_core/src/message.rs b/degeon_core/src/message.rs index f5604d6..9d1f939 100644 --- a/degeon_core/src/message.rs +++ b/degeon_core/src/message.rs @@ -6,8 +6,11 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] #[pyclass] pub struct DegMessage { + /// Sender's public key pub sender: PublicKey, + /// UNIX epoch time at which this message was created pub timestamp: i64, + /// Message content pub content: DegMessageContent, } @@ -32,6 +35,7 @@ impl DegMessage { /// The content of the message #[derive(Clone, Debug, Serialize, Deserialize)] pub enum DegMessageContent { + /// A text message Text(String), File(Vec), Service, @@ -41,8 +45,10 @@ pub enum DegMessageContent { #[derive(Clone, Debug, Serialize, Deserialize, Default)] #[pyclass] pub struct Profile { + /// User's name #[pyo3(get, set)] pub name: String, + /// User's description #[pyo3(get, set)] pub bio: String, } diff --git a/degeon_py/degeon.py b/degeon_py/degeon.py index 35de341..2375924 100644 --- a/degeon_py/degeon.py +++ b/degeon_py/degeon.py @@ -16,7 +16,11 @@ class Degeon: """ The main class with everything connected to the app: the data, the - Attributes + Attributes: + :param core (dc.Degeon): the rust worker + :param chat_selector (ChatSelector): chat list widget + :param active_chat (typing.Optional[ActiveChat]): current chat widget + :param fps (int): FPS rate """ core: 'dc.Degeon' chat_selector: ChatSelector diff --git a/degeon_py/degeon_core.so b/degeon_py/degeon_core.so index cf8c38c..211fda0 100755 Binary files a/degeon_py/degeon_core.so and b/degeon_py/degeon_core.so differ diff --git a/degeon_py/input_field.py b/degeon_py/input_field.py index f6e7f3f..dd9cf54 100644 --- a/degeon_py/input_field.py +++ b/degeon_py/input_field.py @@ -69,7 +69,6 @@ class TextField: elif event.unicode: self.value = self.value[:self.cursor_position] + event.unicode + self.value[self.cursor_position:] self.cursor_position += 1 - # print(self.is_focused, event.type, getattr(event, 'key', None), getattr(event, 'unicode', None), self.value) def collect(self) -> str: """ diff --git a/images/degeon_logo.png b/images/degeon_logo.png new file mode 100644 index 0000000..7e6b8ee Binary files /dev/null and b/images/degeon_logo.png differ diff --git a/images/dependency_graph.png b/images/dependency_graph.png new file mode 100644 index 0000000..fcbfd5b Binary files /dev/null and b/images/dependency_graph.png differ diff --git a/images/dependency_graph.svg b/images/dependency_graph.svg new file mode 100644 index 0000000..6450c54 --- /dev/null +++ b/images/dependency_graph.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + IronForce + + Degeon Core + + Rust Interface + + Python Interface + + + + + + IF + + +