Compare commits
30 Commits
master
...
interface-
Author | SHA1 | Date |
---|---|---|
Lev | 888f77c6bb | 3 years ago |
Lev | f5c61c5aa5 | 3 years ago |
Lev | 456e8940b1 | 3 years ago |
Lev | d7524cb4bf | 3 years ago |
Lev | ff5e99ceb0 | 3 years ago |
Lev | 67b8c7cfe5 | 3 years ago |
Lev | 833e3e4547 | 3 years ago |
Lev | 10e79d57e4 | 3 years ago |
Lev | 4769b2eead | 3 years ago |
Lev | 093e011fc8 | 3 years ago |
Lev | 8680d3379a | 3 years ago |
Lev | df827fc656 | 3 years ago |
Lev | 1caa0324cf | 3 years ago |
Lev | 2574061bed | 3 years ago |
Lev | f0326cea8c | 3 years ago |
Lev | 02c88de37a | 3 years ago |
Lev | af5d27dc97 | 3 years ago |
Lev | 6c69c80d7a | 3 years ago |
Lev | aa15911403 | 3 years ago |
Lev | 82b5973aa0 | 3 years ago |
Lev | 2a7a97f592 | 3 years ago |
Prokhor | 3e25fbb4bb | 3 years ago |
Prokhor | 50ecec2aa3 | 3 years ago |
Lev | 2b6e2b46ad | 3 years ago |
Lev | 07902bb027 | 3 years ago |
Lev | 8f5a562ff8 | 3 years ago |
Lev | 3b17d437d3 | 3 years ago |
Prokhor | 38eb9b2e9e | 3 years ago |
Prokhor | f0fea9d45b | 3 years ago |
Prokhor | b266a0928f | 3 years ago |
29 changed files with 9064 additions and 402 deletions
@ -0,0 +1,12 @@
|
||||
[package] |
||||
name = "degeon" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
iced = { version = "0.3", features = ["glow"] } |
||||
ironforce = { path = "../", features = ["std"] } |
||||
base64 = "0.13.0" |
||||
serde = { version = "1.0" } |
@ -0,0 +1,6 @@
|
||||
#[derive(Clone, Debug)] |
||||
pub enum GuiEvent { |
||||
ChatSelect(usize), |
||||
Typed(String), |
||||
SendClick, |
||||
} |
@ -0,0 +1,70 @@
|
||||
extern crate serde; |
||||
mod message; |
||||
mod state; |
||||
mod gui_events; |
||||
|
||||
use iced::Sandbox; |
||||
use ironforce::res::IFResult; |
||||
use ironforce::{IronForce, Message, MessageType, PublicKey}; |
||||
use crate::state::State; |
||||
|
||||
fn main_if() -> IFResult<()> { |
||||
let ironforce = IronForce::from_file("".to_string())?; |
||||
let if_keys = ironforce.keys.clone(); |
||||
println!( |
||||
"Our public key: {}", |
||||
base64::encode(if_keys.get_public().to_vec().as_slice()) |
||||
); |
||||
let (_thread, if_mutex) = ironforce.launch_main_loop(100); |
||||
let stdin = std::io::stdin(); |
||||
let if_mutex_clone = if_mutex.clone(); |
||||
let if_keys_clone = if_keys.clone(); |
||||
std::thread::spawn(move || loop { |
||||
if let Some(msg) = if_mutex_clone.lock().unwrap().read_message() { |
||||
println!( |
||||
"New message: {}", |
||||
String::from_utf8(msg.get_decrypted(&if_keys_clone).unwrap()).unwrap() |
||||
); |
||||
} |
||||
std::thread::sleep(std::time::Duration::from_millis(300)) |
||||
}); |
||||
loop { |
||||
let mut buf = String::new(); |
||||
stdin.read_line(&mut buf)?; |
||||
let msg_base = if buf.starts_with('>') { |
||||
let target_base64 = buf |
||||
.split(')') |
||||
.next() |
||||
.unwrap() |
||||
.trim_start_matches(">(") |
||||
.to_string(); |
||||
let target = if let Ok(res) = base64::decode(target_base64) { |
||||
res |
||||
} else { |
||||
println!("Wrong b64."); |
||||
continue; |
||||
}; |
||||
buf = buf |
||||
.split(')') |
||||
.skip(1) |
||||
.map(|s| s.to_string()) |
||||
.collect::<Vec<String>>() |
||||
.join(")"); |
||||
Message::build() |
||||
.message_type(MessageType::SingleCast) |
||||
.recipient(&PublicKey::from_vec(target).unwrap()) |
||||
} else { |
||||
Message::build().message_type(MessageType::Broadcast) |
||||
}; |
||||
if_mutex |
||||
.lock() |
||||
.unwrap() |
||||
.send_to_all(msg_base.content(buf.into_bytes()).sign(&if_keys).build()?)?; |
||||
} |
||||
} |
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> { |
||||
// let ironforce = IronForce::from_file("".to_string()).unwrap();
|
||||
// let _if_keys = ironforce.keys.clone();
|
||||
Ok(State::run(iced::Settings::default())?) |
||||
} |
@ -0,0 +1,14 @@
|
||||
use serde::{Deserialize, Serialize}; |
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
pub enum Message { |
||||
Text(String), |
||||
File(Vec<u8>), |
||||
Service(ServiceMsg), |
||||
} |
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||
pub enum ServiceMsg { |
||||
NameRequest, |
||||
NameStatement(String), |
||||
} |
@ -0,0 +1,270 @@
|
||||
use crate::gui_events::GuiEvent; |
||||
use crate::message::Message; |
||||
use core::default::Default; |
||||
use iced::{ |
||||
button, Align, Button, Column, Element, HorizontalAlignment, Length, Row, Sandbox, Settings, |
||||
Text, TextInput, VerticalAlignment, |
||||
}; |
||||
use ironforce::{Keys, PublicKey}; |
||||
use serde::{Deserialize, Serialize}; |
||||
|
||||
#[derive(Clone, Debug)] |
||||
pub struct Chat { |
||||
pkey: PublicKey, |
||||
messages: Vec<(bool, Message)>, |
||||
name: String, |
||||
scrolled: f32, |
||||
pub input: String, |
||||
} |
||||
|
||||
pub fn view_message(msg: &(bool, Message)) -> Option<Element<GuiEvent>> { |
||||
let msg = &msg.1; |
||||
match msg { |
||||
Message::Text(t) => Some( |
||||
iced::Container::new(Text::new(t.as_str())) |
||||
.padding(10) |
||||
.style(style::Container::Message) |
||||
.into(), |
||||
), |
||||
Message::File(_) => None, |
||||
Message::Service(_) => None, |
||||
} |
||||
} |
||||
|
||||
mod style { |
||||
use iced::container::Style; |
||||
use iced::{button, Background, Color, Vector}; |
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)] |
||||
pub enum Button { |
||||
Primary, |
||||
Secondary, |
||||
Destructive, |
||||
InactiveChat, |
||||
} |
||||
|
||||
impl button::StyleSheet for Button { |
||||
fn active(&self) -> button::Style { |
||||
button::Style { |
||||
background: Some(Background::Color(match self { |
||||
Button::Primary => Color::from_rgb(0.11, 0.35, 0.75), |
||||
Button::Secondary => Color::from_rgb(0.3, 0.1, 0.7), |
||||
Button::Destructive => Color::from_rgb(0.8, 0.2, 0.2), |
||||
Button::InactiveChat => Color::from_rgb(0.3, 0.52, 0.9), |
||||
})), |
||||
border_radius: 5.0, |
||||
shadow_offset: Vector::new(1.0, 1.0), |
||||
text_color: if self != &Button::InactiveChat { Color::WHITE } else { Color::BLACK }, |
||||
..button::Style::default() |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub enum Container { |
||||
Primary, |
||||
Message, |
||||
} |
||||
|
||||
impl iced::container::StyleSheet for Container { |
||||
fn style(&self) -> Style { |
||||
iced::container::Style { |
||||
text_color: Some(Color::WHITE), |
||||
background: Some(Background::Color(match self { |
||||
Container::Primary => Color::from_rgb(18. / 256., 25. / 256., 70. / 256.), |
||||
Container::Message => Color::from_rgb(0., 0.1, 0.8), |
||||
})), |
||||
border_radius: 5.0, |
||||
border_width: 0.6, |
||||
border_color: Color::TRANSPARENT, |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl 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) |
||||
.width(Length::Fill) |
||||
.height(Length::Units(50)) |
||||
.padding(10) |
||||
.into() |
||||
} |
||||
|
||||
pub 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> { |
||||
Row::new() |
||||
.width(Length::Fill) |
||||
.padding(15) |
||||
.push( |
||||
TextInput::new(text_input_state, "Message", input.as_str(), |st| { |
||||
GuiEvent::Typed(st) |
||||
}) |
||||
.padding(8) |
||||
.width(Length::Fill), |
||||
) |
||||
.push( |
||||
Button::new(send_button_state, Text::new("Send")) |
||||
.on_press(GuiEvent::SendClick) |
||||
.style(style::Button::Secondary) |
||||
.padding(20) |
||||
.width(Length::Units(80)), |
||||
) |
||||
.spacing(25) |
||||
.height(Length::Units(100)) |
||||
.into() |
||||
} |
||||
|
||||
pub fn preview<'a>(&'a self, state: &'a mut button::State, i: usize, is_selected: bool) -> Element<'a, GuiEvent> { |
||||
Button::new(state, Text::new(self.name.as_str())) |
||||
.width(Length::Fill) |
||||
.padding(10) |
||||
.style(if is_selected { style::Button::Primary } else { style::Button::InactiveChat }) |
||||
.on_press(GuiEvent::ChatSelect(i)) |
||||
.into() |
||||
} |
||||
|
||||
pub 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> { |
||||
let msgs = self.messages.iter().filter_map(view_message).collect(); |
||||
Column::new() |
||||
.align_items(Align::End) |
||||
.height(Length::Fill) |
||||
.width(Length::FillPortion(4)) |
||||
.push(Self::header(self.name.clone())) |
||||
.push( |
||||
Column::with_children(msgs) |
||||
.padding(20) |
||||
.spacing(10) |
||||
.align_items(Align::End) |
||||
.height(Length::FillPortion(9)), |
||||
) |
||||
.spacing(10) |
||||
.push(Self::send_field( |
||||
self.input.to_string(), |
||||
text_input_state, |
||||
send_button_state, |
||||
)) |
||||
.into() |
||||
} |
||||
|
||||
pub fn example(i: usize) -> Chat { |
||||
Self { |
||||
pkey: Keys::generate().get_public(), |
||||
messages: vec![(false, Message::Text(format!("Example message {}", i)))], |
||||
name: format!("Example user ({})", i), |
||||
scrolled: 0.0, |
||||
input: "".to_string(), |
||||
} |
||||
} |
||||
} |
||||
|
||||
#[derive(Default, Clone, Debug)] |
||||
pub struct State { |
||||
chats: Vec<Chat>, |
||||
my_name: String, |
||||
selected_chat: usize, |
||||
pub send_button_state: iced::button::State, |
||||
text_input_state: iced::text_input::State, |
||||
preview_button_states: Vec<button::State>, |
||||
} |
||||
|
||||
impl State { |
||||
fn chat_list<'a>( |
||||
chats: &'a Vec<Chat>, |
||||
preview_button_states: &'a mut Vec<button::State>, |
||||
selected: usize |
||||
) -> Element<'a, GuiEvent> { |
||||
Column::with_children( |
||||
chats |
||||
.iter() |
||||
.zip(preview_button_states.iter_mut()) |
||||
.enumerate() |
||||
.map(|(i, (chat, state))| chat.preview(state, i, i == selected)) |
||||
.collect(), |
||||
) |
||||
.padding(20) |
||||
.spacing(10) |
||||
.align_items(Align::Start) |
||||
.width(Length::FillPortion(1)) |
||||
.into() |
||||
} |
||||
|
||||
pub fn active_chat<'a>( |
||||
chats: &'a Vec<Chat>, |
||||
selected_chat: usize, |
||||
send_button_state: &'a mut button::State, |
||||
text_input_state: &'a mut iced::text_input::State, |
||||
) -> Element<'a, GuiEvent> { |
||||
if selected_chat >= chats.len() { |
||||
Text::new("No chat") |
||||
.horizontal_alignment(HorizontalAlignment::Center) |
||||
.vertical_alignment(VerticalAlignment::Center) |
||||
.width(Length::FillPortion(4)) |
||||
.into() |
||||
} else { |
||||
chats[selected_chat].view(text_input_state, send_button_state) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl Sandbox for State { |
||||
type Message = GuiEvent; |
||||
|
||||
fn new() -> Self { |
||||
let mut st = Self::default(); |
||||
st.chats = vec![Chat::example(1), Chat::example(2)]; |
||||
st.preview_button_states = vec![Default::default(), Default::default()]; |
||||
st |
||||
} |
||||
|
||||
fn title(&self) -> String { |
||||
String::from("Degeon") |
||||
} |
||||
|
||||
fn update(&mut self, message: GuiEvent) { |
||||
match message { |
||||
GuiEvent::ChatSelect(i) => self.selected_chat = i, |
||||
GuiEvent::Typed(st) => self.chats[self.selected_chat].input = st, |
||||
GuiEvent::SendClick => { |
||||
if self.chats[self.selected_chat].input.is_empty() { |
||||
return; |
||||
} |
||||
let new_msg = Message::Text(self.chats[self.selected_chat].input.clone()); |
||||
self.chats[self.selected_chat].input = String::new(); |
||||
self.chats[self.selected_chat] |
||||
.messages |
||||
.push((true, new_msg)); |
||||
// todo
|
||||
} |
||||
} |
||||
} |
||||
|
||||
fn view(&mut self) -> Element<GuiEvent> { |
||||
let Self { |
||||
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() |
||||
} |
||||
} |
Binary file not shown.
@ -0,0 +1,103 @@
|
||||
from __future__ import annotations |
||||
|
||||
import typing |
||||
from dataclasses import dataclass, field |
||||
import pygame |
||||
|
||||
from button import Button |
||||
from config import HEIGHT, WIDTH, CHAT_SELECTOR_WIDTH, DARKER_BLUE, CHAT_PREVIEW_HEIGHT, WHITE, font, DARK_BLUE, \ |
||||
MESSAGE_HEIGHT |
||||
import degeon_core as dc |
||||
|
||||
from input_field import TextField |
||||
from message import Message |
||||
|
||||
|
||||
@dataclass |
||||
class ActiveChat: |
||||
""" |
||||
The widget with the current chat |
||||
|
||||
Attributes: |
||||
:param chat (dc.Chat): the chat (Rust Chat type) |
||||
:param height (int): the height of the active chat widget |
||||
:param width (int): its width |
||||
:param delta_x (int): distance from the left edge of the application to the left edge of the screen |
||||
:param header_height (int): height of the header (the title) |
||||
""" |
||||
chat: dc.Chat |
||||
input_field: TextField = field(default_factory=lambda: TextField()) |
||||
send_button: Button = field(default_factory=lambda: Button(height=CHAT_PREVIEW_HEIGHT, width=100, text='Send', |
||||
top_left=(WIDTH - 112, HEIGHT - 85))) |
||||
height: int = HEIGHT |
||||
width: int = WIDTH - CHAT_SELECTOR_WIDTH - 10 |
||||
delta_x: int = CHAT_SELECTOR_WIDTH + 10 |
||||
header_height: int = int(CHAT_PREVIEW_HEIGHT * 1.5) |
||||
|
||||
@classmethod |
||||
def new(cls, chat: dc.Chat, **kwargs) -> ActiveChat: |
||||
""" |
||||
Create a new `Chat` from a rust Chat object |
||||
:param chat: rusty chat |
||||
:param kwargs: optional other paraeters |
||||
:return: the `Chat` |
||||
""" |
||||
return cls(chat=chat, **kwargs) |
||||
|
||||
def get_messages(self) -> typing.Iterable[Message]: |
||||
""" |
||||
Get an iterator over all messages in this chat in the backwards order |
||||
This function creates a python `message.Message` object from rust instances |
||||
:return: an iterator of `message.Message` objects |
||||
""" |
||||
for msg in reversed(self.chat.messages): |
||||
yield Message(text=msg.get_content_py().text, is_from_me=False) |
||||
|
||||
def get_header(self) -> pygame.Surface: |
||||
""" |
||||
Render a pygame surface with the header. |
||||
The header is (for now) just a name of the user written on a background |
||||
|
||||
:return: the header |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.header_height)) |
||||
surface.fill(DARK_BLUE) |
||||
name_surface: pygame.Surface = font.render(self.chat.profile.name, True, WHITE) |
||||
surface.blit(name_surface, (20, 20)) |
||||
return surface |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Creates a pygame surface and draws the chat on it |
||||
:return: the surface with the chat on it |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.height)) |
||||
surface.fill(DARKER_BLUE) |
||||
|
||||
# Render messages |
||||
# This is the y0 for the last message |
||||
last_message_y = self.height - MESSAGE_HEIGHT * 2 |
||||
for i, message in zip(range(30), self.get_messages()): |
||||
msg_surface = message.render() |
||||
surface.blit(msg_surface, (0, last_message_y - (MESSAGE_HEIGHT + 30) * (i + 1))) |
||||
# Render header |
||||
header = self.get_header() |
||||
surface.blit(header, (0, 10)) |
||||
# Render message input |
||||
input_field_surface: pygame.Surface = self.input_field.render() |
||||
surface.blit(input_field_surface, (0, self.height - input_field_surface.get_height())) |
||||
# Render sending button |
||||
sending_button_surface: pygame.Surface = self.send_button.render() |
||||
surface.blit(sending_button_surface, |
||||
(self.send_button.top_left[0] - self.delta_x, self.send_button.top_left[1])) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event) -> typing.Optional[str]: |
||||
""" |
||||
Process clicks in the active chat widget. Return a message to send if needed |
||||
:param event: a pygame event |
||||
:return: A message to send if there is one |
||||
""" |
||||
self.input_field.process_event(event) |
||||
if self.send_button.process_event(event): |
||||
return self.input_field.collect() |
@ -0,0 +1,99 @@
|
||||
import pygame |
||||
import typing |
||||
|
||||
from config import font_large, WHITE, BLUE, BLACK |
||||
from dataclasses import dataclass |
||||
|
||||
|
||||
@dataclass |
||||
class Button: |
||||
""" |
||||
Just a button element for pygame |
||||
Attributes: |
||||
:param text (str): Text on the button |
||||
:param top_left ((int, int)): the coordinates of the top-left point of the button |
||||
:param width (int): the width of the button |
||||
:param height (int): the height of the button |
||||
:param padding (int): padding - the distance between rectangle border and button content |
||||
:param bg_color (Color): the background color |
||||
:param text_color (Color): color of the text |
||||
:param hovered_color (Color): the background color when the button is hovered |
||||
:param hovered_text_color (Color): the text color when the button is hovered |
||||
:param pressed_color (Color): the background color when the button is pressed |
||||
:param pressed_text_color (Color): the text color when the button is pressed |
||||
""" |
||||
text: str |
||||
top_left: typing.Tuple[int, int] |
||||
width: int |
||||
height: int |
||||
padding: int = 13 |
||||
bg_color: pygame.Color = BLUE |
||||
text_color: pygame.Color = WHITE |
||||
hovered_color: pygame.Color = (BLUE * 3 + WHITE) // 4 |
||||
hovered_text_color: pygame.Color = BLACK |
||||
pressed_color: pygame.Color = (BLUE + WHITE * 3) // 4 |
||||
pressed_text_color: pygame.Color = BLACK |
||||
is_hovered: bool = False |
||||
is_pressed: bool = False |
||||
|
||||
@property |
||||
def bottom(self) -> int: |
||||
""" |
||||
Return the y coordinate of the bottom of the button |
||||
:return: y_bottom |
||||
""" |
||||
return self.top_left[1] + self.height |
||||
|
||||
@property |
||||
def right(self) -> int: |
||||
""" |
||||
Return the x coordinate of the right edge of the button |
||||
:return: x_right |
||||
""" |
||||
return self.top_left[0] + self.width |
||||
|
||||
def get_colors(self) -> typing.Tuple[pygame.Color, pygame.Color]: |
||||
""" |
||||
Get background and text color considering hovered and pressed states |
||||
:return: the button's background color and the button's text color |
||||
""" |
||||
if self.is_pressed: |
||||
return self.pressed_color, self.pressed_text_color |
||||
if self.is_hovered: |
||||
return self.hovered_color, self.hovered_text_color |
||||
return self.bg_color, self.text_color |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Draw the button |
||||
:return: a pygame surface with the button |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.height)) |
||||
bg_color, text_color = self.get_colors() |
||||
surface.fill(bg_color) |
||||
text_surface: pygame.Surface = font_large.render(self.text, True, text_color) |
||||
text_height: int = self.height - 2 * self.padding |
||||
text_width: int = round(text_surface.get_width() * text_height / text_surface.get_height()) |
||||
text_surface: pygame.Surface = pygame.transform.scale(text_surface, (text_width, text_height)) |
||||
surface.blit(text_surface, ((self.width - text_width) // 2, self.padding)) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event) -> bool: |
||||
""" |
||||
Process a pygame event. If it's a click on this button, return true |
||||
:param event: a pygame event |
||||
:return: True if there was a click on the button |
||||
""" |
||||
# if this is a mouse event |
||||
if event.type in [pygame.MOUSEBUTTONUP, pygame.MOUSEMOTION, pygame.MOUSEBUTTONDOWN]: |
||||
# if the mouse event is inside the button |
||||
if self.top_left[0] <= event.pos[0] < self.bottom and self.top_left[1] <= event.pos[1] < self.right: |
||||
if event.type == pygame.MOUSEBUTTONUP: |
||||
self.is_pressed = False |
||||
return True |
||||
elif event.type == pygame.MOUSEBUTTONDOWN: |
||||
self.is_hovered = False |
||||
self.is_pressed = True |
||||
elif event.type == pygame.MOUSEMOTION: |
||||
self.is_hovered = True |
||||
return False |
@ -0,0 +1,76 @@
|
||||
from __future__ import annotations |
||||
|
||||
import typing |
||||
from dataclasses import dataclass, field |
||||
|
||||
import pygame |
||||
|
||||
from config import CHAT_SELECTOR_WIDTH, HEIGHT, DARK_BLUE, WHITE, GREY, BLUE, font, CHAT_PREVIEW_HEIGHT, MEDIUM_BLUE |
||||
|
||||
|
||||
@dataclass |
||||
class ChatSelector: |
||||
""" |
||||
The widget with the list of chats. |
||||
It's a dataclass, it should be initialized using the `from_chats` classmethod |
||||
|
||||
Attributes: |
||||
chats (List[dc.Chat]): list of all chats, where each chat is a native Rust struct Chat |
||||
active_chat (int): the index of the current selected chat |
||||
hovered_chat (int or None): the index of the current hovered chat |
||||
width (int): the width of this widget |
||||
height (int): the height of this widget |
||||
chat_height (int): height of one chat |
||||
""" |
||||
chats: typing.List['dc.Chat'] = field(default_factory=list) |
||||
active_chat: int = -1 |
||||
hovered_chat: typing.Optional[int] = None |
||||
width: int = CHAT_SELECTOR_WIDTH |
||||
height: int = HEIGHT |
||||
chat_height: int = CHAT_PREVIEW_HEIGHT |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Creates a pygame surface and draws the list of chats on it |
||||
:return: the surface with the chat selector |
||||
""" |
||||
surface: pygame.Surface = pygame.Surface((self.width, self.height)) |
||||
surface.fill(GREY) |
||||
for i, chat in enumerate(self.chats): |
||||
bg_color, text_color = DARK_BLUE, WHITE |
||||
if i == self.hovered_chat: |
||||
bg_color = MEDIUM_BLUE |
||||
if i == self.active_chat: |
||||
bg_color = BLUE |
||||
title_surface: pygame.Surface = font.render(chat.profile.name, True, text_color) |
||||
pygame.draw.rect(surface, bg_color, (3, i * self.chat_height + 1, self.width - 6, self.chat_height - 2)) |
||||
surface.blit(title_surface, (7, i * self.chat_height + 10)) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event) -> bool: |
||||
""" |
||||
Process a click: select the necessary chat if this click is in the widget |
||||
:param event: a pygame event |
||||
:return: True if a chat was changed |
||||
""" |
||||
if event.type == pygame.MOUSEMOTION: |
||||
if 0 < event.pos[0] < self.width \ |
||||
and 0 < event.pos[1] < min(self.height, len(self.chats) * self.chat_height) - 2: |
||||
self.hovered_chat = event.pos[1] // self.chat_height |
||||
else: |
||||
self.hovered_chat = None |
||||
|
||||
if event.type == pygame.MOUSEBUTTONUP and event.pos[0] < self.width: |
||||
self.hovered_chat = None |
||||
self.active_chat = event.pos[1] // self.chat_height |
||||
return True |
||||
return False |
||||
|
||||
@classmethod |
||||
def from_chats(cls, chats: typing.List['dc.Chat'], **kwargs) -> ChatSelector: |
||||
""" |
||||
Create a new ChatSelector from a list of Rust chats |
||||
:param chats: the list of chats |
||||
:param kwargs: optional additional arguments |
||||
""" |
||||
return cls(chats, **kwargs) |
@ -0,0 +1,31 @@
|
||||
import pygame |
||||
|
||||
|
||||
pygame.init() |
||||
|
||||
# Fontss |
||||
font = pygame.font.Font('CRC35.otf', 32) |
||||
font_large = pygame.font.Font('CRC35.otf', 50) |
||||
|
||||
# Colors used in the app |
||||
RED = 0xFF0000 |
||||
BLUE = 0x0000FF |
||||
YELLOW = 0xFFC91F |
||||
GREEN = 0x00FF00 |
||||
MAGENTA = 0xFF03B8 |
||||
CYAN = 0x00FFCC |
||||
BLACK = 0x000 |
||||
WHITE = 0xFFFFFF |
||||
MEDIUM_BLUE = 0x2f2f4e |
||||
DARK_BLUE = 0x282e46 |
||||
DARKER_BLUE = 0x202033 |
||||
GREY = 0x383e4F |
||||
|
||||
# Geometrical parameters |
||||
WIDTH = 1000 |
||||
HEIGHT = 800 |
||||
CHAT_PREVIEW_HEIGHT = 80 |
||||
CHAT_SELECTOR_WIDTH = WIDTH // 3 |
||||
MESSAGE_HEIGHT = 60 |
||||
|
||||
FPS = 30 |
@ -0,0 +1,96 @@
|
||||
from __future__ import annotations |
||||
|
||||
from dataclasses import dataclass, field |
||||
import typing |
||||
import pygame |
||||
|
||||
from chat_selector import ChatSelector |
||||
from active_chat import ActiveChat |
||||
from config import FPS, DARKER_BLUE, font, WHITE, WIDTH, CHAT_SELECTOR_WIDTH, HEIGHT |
||||
|
||||
import degeon_core as dc |
||||
|
||||
|
||||
@dataclass |
||||
class Degeon: |
||||
""" |
||||
The main class with everything connected to the app: the data, the |
||||
|
||||
Attributes |
||||
""" |
||||
core: 'dc.Degeon' |
||||
chat_selector: ChatSelector |
||||
active_chat: typing.Optional[ActiveChat] = None |
||||
has_profile_popup_opened: bool = False |
||||
has_no_peers_popup: bool = False |
||||
clock: pygame.time.Clock = field(default_factory=pygame.time.Clock) |
||||
fps: int = FPS |
||||
|
||||
@classmethod |
||||
def new(cls) -> Degeon: |
||||
""" |
||||
Create a new default instance with settings from file |
||||
:return: a Degeon instance |
||||
""" |
||||
core: dc.Degeon = dc.new_degeon() |
||||
chat_selector = ChatSelector() |
||||
return cls(core=core, chat_selector=chat_selector) |
||||
|
||||
def render(self, screen: pygame.Surface): |
||||
""" |
||||
Render everything on the screen |
||||
:param screen: the main screen |
||||
""" |
||||
chats_surface = self.chat_selector.render() |
||||
screen.blit(chats_surface, (0, 0)) |
||||
if self.active_chat is not None: |
||||
active_chat_surface = self.active_chat.render() |
||||
screen.blit(active_chat_surface, (self.active_chat.delta_x, 0)) |
||||
else: |
||||
text_surface: pygame.Surface = font.render('<- Select chat in the menu', True, WHITE) |
||||
screen.blit(text_surface, |
||||
(round(WIDTH / 2 + CHAT_SELECTOR_WIDTH / 2 - text_surface.get_width() / 2), HEIGHT // 2)) |
||||
|
||||
def process_core_messages(self): |
||||
""" |
||||
Do all the necessary Rust work |
||||
""" |
||||
pass # todo |
||||
|
||||
def tick(self): |
||||
""" |
||||
Handle incoming messages, update chats, create no_peers popup if necessary |
||||
""" |
||||
# process events in core |
||||
self.process_core_messages() |
||||
self.chat_selector.chats = self.core.chats |
||||
if 0 <= self.chat_selector.active_chat < len(self.chat_selector.chats) and self.active_chat is None: |
||||
self.active_chat = ActiveChat.new(self.chat_selector.chats[self.chat_selector.active_chat]) |
||||
|
||||
def process_event(self, event: pygame.event.Event): |
||||
""" |
||||
Process an event |
||||
:param event: pygame event |
||||
""" |
||||
if self.chat_selector.process_event(event): |
||||
if 0 <= self.chat_selector.active_chat < len(self.chat_selector.chats): |
||||
self.active_chat = ActiveChat.new(self.chat_selector.chats[self.chat_selector.active_chat]) |
||||
else: |
||||
self.active_chat = None |
||||
if self.active_chat is not None: |
||||
self.active_chat.process_event(event) |
||||
|
||||
def main_loop(self, screen: pygame.Surface): |
||||
""" |
||||
Drawing everything and handling events |
||||
""" |
||||
while True: |
||||
screen.fill(DARKER_BLUE) |
||||
for event in pygame.event.get(): |
||||
if event.type == pygame.QUIT: |
||||
return |
||||
self.process_event(event) |
||||
self.tick() |
||||
self.render(screen) |
||||
self.clock.tick(self.fps) |
||||
pygame.display.update() |
@ -0,0 +1,82 @@
|
||||
import typing |
||||
|
||||
import pygame |
||||
|
||||
from config import font, MESSAGE_HEIGHT, WIDTH, CHAT_SELECTOR_WIDTH, HEIGHT, WHITE, DARKER_BLUE, GREY |
||||
|
||||
from dataclasses import dataclass |
||||
|
||||
|
||||
@dataclass |
||||
class TextField: |
||||
""" |
||||
Field for message input |
||||
""" |
||||
value: str = '' |
||||
width: int = WIDTH - CHAT_SELECTOR_WIDTH - 140 |
||||
height: int = MESSAGE_HEIGHT * 1.5 |
||||
top_left_corner: typing.Tuple[int, int] = (CHAT_SELECTOR_WIDTH, HEIGHT - MESSAGE_HEIGHT * 2) |
||||
is_focused: bool = False |
||||
placeholder: str = '' |
||||
cursor_position: int = 0 |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Render the text field onto a pygame surface |
||||
:return: a surface with the field |
||||
""" |
||||
surface = pygame.Surface((self.width, self.height)) |
||||
surface.fill(WHITE) |
||||
padding = 5 |
||||
pygame.draw.rect(surface, DARKER_BLUE, (padding, padding, self.width - padding * 2, self.height - padding * 2)) |
||||
if not self.value and self.placeholder: |
||||
placeholder_text: pygame.Surface = font.render(self.placeholder, True, GREY) |
||||
surface.blit( |
||||
placeholder_text, |
||||
( |
||||
(self.width - placeholder_text.get_width()) // 2, |
||||
(self.height - placeholder_text.get_height()) // 2 |
||||
) |
||||
) |
||||
if self.value: |
||||
message_text: pygame.Surface = font.render(self.value, True, WHITE) |
||||
surface.blit( |
||||
message_text, |
||||
( |
||||
40, # or, if we want to center, `(self.width - message_text.get_width()) // 2,` |
||||
(self.height - message_text.get_height()) // 2 |
||||
) |
||||
) |
||||
return surface |
||||
|
||||
def process_event(self, event: pygame.event.Event): |
||||
""" |
||||
Handle a typing event or a click (to focus) |
||||
:param event: a pygame event |
||||
""" |
||||
# If we have a click, we should focus the field if the click was inside or unfocus if it was outside |
||||
if event.type == pygame.MOUSEBUTTONUP: |
||||
self.is_focused = self.top_left_corner[0] <= event.pos[0] < self.top_left_corner[0] + self.width \ |
||||
and self.top_left_corner[1] <= event.pos[1] < self.top_left_corner[1] + self.height |
||||
if self.is_focused and hasattr(event, 'key') and event.type == 768: |
||||
if event.key == pygame.K_BACKSPACE: |
||||
self.value = self.value[:-1] |
||||
self.cursor_position -= 1 |
||||
elif event.key == pygame.K_LEFT: |
||||
self.cursor_position -= 1 |
||||
elif event.key == pygame.K_RIGHT: |
||||
self.cursor_position += 1 |
||||
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: |
||||
""" |
||||
Get the current value and clear the field |
||||
:return: the value of the text input |
||||
""" |
||||
value = self.value |
||||
self.value = '' |
||||
self.cursor_position = 0 |
||||
return value |
@ -0,0 +1,11 @@
|
||||
from __future__ import annotations |
||||
|
||||
import pygame |
||||
from degeon import Degeon |
||||
from config import WIDTH, HEIGHT |
||||
|
||||
|
||||
deg = Degeon.new() |
||||
screen = pygame.display.set_mode((WIDTH, HEIGHT)) |
||||
deg.main_loop(screen) |
||||
pygame.quit() |
@ -0,0 +1,42 @@
|
||||
from __future__ import annotations |
||||
|
||||
from dataclasses import dataclass |
||||
|
||||
import pygame |
||||
|
||||
from config import WIDTH, CHAT_SELECTOR_WIDTH, MESSAGE_HEIGHT, font, BLUE, DARK_BLUE, WHITE, DARKER_BLUE |
||||
|
||||
|
||||
@dataclass |
||||
class Message: |
||||
""" |
||||
The message (for now, it consists only of text) |
||||
|
||||
Attributes: |
||||
:param text (str): the message text |
||||
:param is_from_me (bool): False if the message is not from the current user |
||||
:param chat_width (int): the width of the active chat widget |
||||
""" |
||||
text: str |
||||
is_from_me: bool |
||||
chat_width: int = WIDTH - CHAT_SELECTOR_WIDTH - 10 |
||||
height: int = MESSAGE_HEIGHT |
||||
|
||||
def render(self) -> pygame.Surface: |
||||
""" |
||||
Creates a surface with a rectangle and the message text written on it |
||||
:return: the surface with rendered message |
||||
""" |
||||
surface = pygame.Surface((self.chat_width, self.height)) |
||||
surface.fill(DARKER_BLUE) |
||||
bg_color = BLUE * self.is_from_me + DARK_BLUE * (not self.is_from_me) |
||||
text_surface: pygame.Surface = font.render(self.text, True, WHITE) |
||||
padding = 5 |
||||
# Size of the scaled text surface |
||||
blit_height: int = self.height - padding * 2 |
||||
blit_width: int = round(text_surface.get_width() * blit_height / text_surface.get_height()) |
||||
x: int = 0 if not self.is_from_me else self.chat_width - blit_width - padding * 2 |
||||
pygame.draw.rect(surface, bg_color, (x, 0, blit_width + padding * 2, self.height)) |
||||
text_surface: pygame.Surface = pygame.transform.smoothscale(text_surface, (blit_width, blit_height)) |
||||
surface.blit(text_surface, (x + padding, padding)) |
||||
return surface |
@ -0,0 +1,49 @@
|
||||
#[cfg(feature = "std")] |
||||
use ironforce::interface::Interface; |
||||
#[cfg(feature = "std")] |
||||
use ironforce::interfaces::ip::IPInterface; |
||||
#[cfg(feature = "std")] |
||||
use std::net; |
||||
use std::thread; |
||||
use std::time::Duration; |
||||
#[cfg(feature = "std")] |
||||
use ironforce::res::IFResult; |
||||
|
||||
#[cfg(feature = "std")] |
||||
fn main() { |
||||
println!("hello"); |
||||
test(); |
||||
} |
||||
|
||||
#[cfg(feature = "std")] |
||||
fn test() -> IFResult<()> { |
||||
let message = *b"Hello world from iron forest"; |
||||
|
||||
let mut interface1 = IPInterface::new()?; |
||||
let mut interface2 = IPInterface::new()?; |
||||
|
||||
let t1 = std::thread::spawn(move || { |
||||
interface1.send(&message, Some(String::from("127.0.0.1:50001"))).unwrap(); |
||||
interface1 |
||||
}); |
||||
thread::sleep(Duration::from_millis(10)); |
||||
let t2 = std::thread::spawn(move || { |
||||
|
||||
interface2.main_loop_iteration().unwrap(); |
||||
interface2 |
||||
}); |
||||
let res1 = t1.join(); |
||||
match res1 { |
||||
Ok(_) => println!("Ok"), |
||||
Err(e) => println!("{:?}", e) |
||||
} |
||||
let res2 = t2.join(); |
||||
match res2 { |
||||
Ok(_) => println!("Ok"), |
||||
Err(e) => println!("{:?}", e) |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
#[cfg(not(feature = "std"))] |
||||
fn main() {} |
@ -0,0 +1,57 @@
|
||||
use ironforce::res::IFResult; |
||||
use ironforce::{IronForce, Message, MessageType, PublicKey}; |
||||
|
||||
fn main() -> IFResult<()> { |
||||
let ironforce = IronForce::from_file("".to_string())?; |
||||
let if_keys = ironforce.keys.clone(); |
||||
println!( |
||||
"Our public key: {}", |
||||
base64::encode(if_keys.get_public().to_vec().as_slice()) |
||||
); |
||||
let (_thread, if_mutex) = ironforce.launch_main_loop(100); |
||||
let stdin = std::io::stdin(); |
||||
let if_mutex_clone = if_mutex.clone(); |
||||
let if_keys_clone = if_keys.clone(); |
||||
std::thread::spawn(move || loop { |
||||
if let Some(msg) = if_mutex_clone.lock().unwrap().read_message() { |
||||
println!( |
||||
"New message: {}", |
||||
String::from_utf8(msg.get_decrypted(&if_keys_clone).unwrap()).unwrap() |
||||
); |
||||
} |
||||
std::thread::sleep(std::time::Duration::from_millis(300)) |
||||
}); |
||||
loop { |
||||
let mut buf = String::new(); |
||||
stdin.read_line(&mut buf)?; |
||||
let msg_base = if buf.starts_with('>') { |
||||
let target_base64 = buf |
||||
.split(')') |
||||
.next() |
||||
.unwrap() |
||||
.trim_start_matches(">(") |
||||
.to_string(); |
||||
let target = if let Ok(res) = base64::decode(target_base64) { |
||||
res |
||||
} else { |
||||
println!("Wrong b64."); |
||||
continue; |
||||
}; |
||||
buf = buf |
||||
.split(')') |
||||
.skip(1) |
||||
.map(|s| s.to_string()) |
||||
.collect::<Vec<String>>() |
||||
.join(")"); |
||||
Message::build() |
||||
.message_type(MessageType::SingleCast) |
||||
.recipient(&PublicKey::from_vec(target).unwrap()) |
||||
} else { |
||||
Message::build().message_type(MessageType::Broadcast) |
||||
}; |
||||
if_mutex |
||||
.lock() |
||||
.unwrap() |
||||
.send_to_all(msg_base.content(buf.into_bytes()).sign(&if_keys).build()?)?; |
||||
} |
||||
} |
@ -0,0 +1,507 @@
|
||||
use alloc::borrow::ToOwned; |
||||
use alloc::string::{String, ToString}; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use core::ops::RangeInclusive; |
||||
use core::str::FromStr; |
||||
use core::time::Duration; |
||||
use rayon::prelude::*; |
||||
use serde::{Deserialize, Serialize}; |
||||
use std::net::TcpStream; |
||||
use std::{format, net}; |
||||
|
||||
use crate::interface::{Interface, InterfaceRequirements, TargetingData}; |
||||
use crate::message::MessageBytes; |
||||
use crate::res::{IFError, IFResult}; |
||||
use crate::std::io::{Read, Write}; |
||||
use crate::std::println; |
||||
|
||||
pub const DEFAULT_PORT: u16 = 50000; |
||||
const SOCKET_RANGE: RangeInclusive<u16> = 50000..=50010; |
||||
|
||||
/// The threshold for the number of peers below which we are desperate
|
||||
const PEER_THRESHOLD: usize = 70; |
||||
|
||||
type Peer = (net::IpAddr, u16); |
||||
|
||||
/// Interface for interactions using tcp sockets
|
||||
pub struct IPInterface { |
||||
id: String, |
||||
connections: Vec<net::TcpStream>, |
||||
listener: net::TcpListener, |
||||
peers: Vec<Peer>, |
||||
package_queue: Vec<(IPPackage, String /* from_peer */)>, |
||||
main_loop_iterations: u64, |
||||
} |
||||
|
||||
/// Data for the serialization of `IPInterface`
|
||||
#[derive(Serialize, Deserialize)] |
||||
pub struct SerData { |
||||
pub peers: Vec<Peer>, |
||||
pub port: u16, |
||||
} |
||||
|
||||
#[derive(Debug, Clone)] |
||||
struct IPPackage { |
||||
version: u8, |
||||
package_type: MessageType, |
||||
size: u32, |
||||
message: MessageBytes, |
||||
} |
||||
|
||||
#[derive(Debug, Copy, Clone)] |
||||
enum MessageType { |
||||
Common, |
||||
PeerRequest, |
||||
PeersShared, |
||||
} |
||||
|
||||
impl MessageType { |
||||
fn from_u8(id: u8) -> IFResult<MessageType> { |
||||
match id { |
||||
0 => Ok(MessageType::Common), |
||||
1 => Ok(MessageType::PeerRequest), |
||||
2 => Ok(MessageType::PeersShared), |
||||
_ => Err(IFError::General("Incorrect message type".to_string())), |
||||
} |
||||
} |
||||
|
||||
fn as_u8(&self) -> u8 { |
||||
match self { |
||||
MessageType::Common => 0, |
||||
MessageType::PeerRequest => 1, |
||||
MessageType::PeersShared => 2, |
||||
} |
||||
} |
||||
} |
||||
|
||||
fn compare_addrs(peer: &Peer, addr: net::SocketAddr) -> bool { |
||||
addr.ip() == peer.0 && addr.port() == peer.1 |
||||
} |
||||
|
||||
impl InterfaceRequirements for IPInterface {} |
||||
|
||||
impl Interface for IPInterface { |
||||
fn main_loop_iteration(&mut self) -> IFResult<()> { |
||||
if let Some(conn) = self.listener.incoming().next() { |
||||
match conn { |
||||
Ok(stream) => { |
||||
stream.set_nonblocking(true)?; |
||||
let addr = stream.peer_addr()?; |
||||
println!( |
||||
"({:?}): New client: {:?}", |
||||
addr, |
||||
self.listener.local_addr().unwrap() |
||||
); |
||||
if self.peers.iter().all(|(ip, _)| *ip != addr.ip()) { |
||||
self.peers.push((addr.ip(), addr.port())); |
||||
} |
||||
self.connections.push(stream) |
||||
} |
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {} |
||||
Err(e) => return Err(IFError::from(e)), |
||||
} |
||||
} |
||||
let mut new_connections: Vec<TcpStream> = vec![]; |
||||
for connection in &mut self.connections { |
||||
connection.set_nonblocking(true)?; |
||||
let mut buf = [0u8; 6]; |
||||
let peek_res = connection.peek(&mut buf); |
||||
if peek_res.is_err() || peek_res.unwrap() < 6 { |
||||
continue
|
||||
} |
||||
let mut header: [u8; 6] = [0, 0, 0, 0, 0, 0]; |
||||
match connection.read_exact(&mut header) { |
||||
Ok(_) => {} |
||||
Err(ref e) |
||||
if e.kind() == std::io::ErrorKind::WouldBlock |
||||
|| e.kind() == std::io::ErrorKind::UnexpectedEof => |
||||
{ |
||||
continue
|
||||
} |
||||
Err(e) => { |
||||
println!("Error: {:?}", e); |
||||
continue; |
||||
} |
||||
}; |
||||
let version = header[0]; |
||||
let package_type = MessageType::from_u8(header[1])?; |
||||
let size = bytes_to_size([header[2], header[3], header[4], header[5]]); |
||||
connection.set_nonblocking(false)?; |
||||
connection.set_read_timeout(Some(std::time::Duration::from_millis(500)))?; |
||||
|
||||
let mut message_take = connection.take(size as u64); |
||||
let mut message: Vec<u8> = vec![]; |
||||
message_take.read_to_end(&mut message)?; |
||||
|
||||
match package_type { |
||||
MessageType::PeerRequest => { |
||||
let peers_to_share = if self.peers.len() < PEER_THRESHOLD { |
||||
self.peers.clone() |
||||
} else { |
||||
self.peers.iter().skip(7).step_by(2).cloned().collect() |
||||
}; |
||||
let message = serde_cbor::to_vec(&peers_to_share)?; |
||||
IPInterface::send_package( |
||||
connection, |
||||
IPPackage { |
||||
version, |
||||
package_type: MessageType::PeersShared, |
||||
size: message.len() as u32, |
||||
message, |
||||
}, |
||||
)?; |
||||
} |
||||
MessageType::Common => { |
||||
let package = IPPackage { |
||||
version, |
||||
package_type, |
||||
size, |
||||
message, |
||||
}; |
||||
self.package_queue |
||||
.push((package, format!("{:?}", connection.peer_addr()?))); |
||||
} |
||||
MessageType::PeersShared => { |
||||
let peers: Vec<Peer> = serde_cbor::from_slice(message.as_slice())?; |
||||
for peer in peers { |
||||
if !self.peers.contains(&peer) { |
||||
if let Some(conn) = IPInterface::new_connection(&peer)? { |
||||
new_connections.push(conn) |
||||
} |
||||
self.peers.push(peer); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
for conn in new_connections.iter_mut() { |
||||
self.initialize_connection(conn)?; |
||||
} |
||||
self.connections.extend(new_connections); |
||||
|
||||
self.main_loop_iterations += 1; |
||||
// Every 50 iterations we connect to everyone we know
|
||||
if self.main_loop_iterations % 50 == 0 { |
||||
let connected_addresses = self |
||||
.connections |
||||
.iter() |
||||
.filter_map(|conn| conn.peer_addr().ok()) |
||||
.collect::<Vec<_>>(); |
||||
let peers_we_do_not_have_connections_with = self |
||||
.peers |
||||
.iter() |
||||
.filter(|p| { |
||||
!connected_addresses |
||||
.iter() |
||||
.any(|addr| compare_addrs(p, *addr)) |
||||
}) |
||||
.copied() |
||||
.collect::<Vec<_>>(); |
||||
self.connections |
||||
.extend(IPInterface::get_connections_to_peers( |
||||
&peers_we_do_not_have_connections_with, |
||||
self.peers.len() < PEER_THRESHOLD * 2, |
||||
)); |
||||
} |
||||
// We do a peer exchange every 30 iterations
|
||||
if self.main_loop_iterations % 30 == 0 && !self.connections.is_empty() { |
||||
let connection_index = |
||||
(self.main_loop_iterations / 30) as usize % self.connections.len(); |
||||
IPInterface::request_peers(&mut self.connections[connection_index])?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
fn has_blocking_main(&self) -> bool { |
||||
false |
||||
} |
||||
fn id(&self) -> &str { |
||||
&*self.id |
||||
} |
||||
fn send(&mut self, message: &[u8], interface_data: Option<TargetingData>) -> IFResult<()> { |
||||
|
||||
let package = IPPackage { |
||||
version: 0, |
||||
package_type: MessageType::Common, |
||||
size: message.len() as u32, |
||||
message: Vec::from(message), |
||||
}; |
||||
|
||||
match interface_data { |
||||
Some(ip_string) => { |
||||
let addr: net::SocketAddr = ip_string.parse().expect("Unable to parse address"); |
||||
let index = self.obtain_connection(&(addr.ip(), addr.port()))?; |
||||
IPInterface::send_package(&mut self.connections[index], package)?; |
||||
} |
||||
None => { |
||||
for conn in &mut self.connections { |
||||
IPInterface::send_package(conn, package.clone())?; |
||||
} |
||||
} |
||||
}; |
||||
Ok(()) |
||||
} |
||||
fn receive(&mut self) -> IFResult<Option<(MessageBytes, TargetingData)>> { |
||||
// if !self.package_queue.is_empty() {
|
||||
// println!(
|
||||
// "({:?}): New message from {}. By the way, I know {} peers and have {} connections",
|
||||
// self.listener.local_addr().unwrap(),
|
||||
// self.package_queue.last().unwrap().1,
|
||||
// self.peers.len(),
|
||||
// self.connections.len()
|
||||
// );
|
||||
// }
|
||||
match self.package_queue.pop() { |
||||
Some((ip_package, data)) => Ok(Some((ip_package.message, data))), |
||||
None => Ok(None), |
||||
} |
||||
} |
||||
|
||||
fn get_dump_data(&self) -> String { |
||||
let data = SerData { |
||||
peers: self.peers.clone(), |
||||
port: self.listener.local_addr().unwrap().port(), |
||||
}; |
||||
serde_json::to_string(&data).unwrap() |
||||
} |
||||
|
||||
fn from_dump(data: String) -> IFResult<Self> { |
||||
if !data.is_empty() { |
||||
let data: SerData = serde_json::from_str(data.as_str()).unwrap(); |
||||
IPInterface::new(data.port, data.peers) |
||||
} else { |
||||
let ip_path = std::path::Path::new(".if_ip_peers"); |
||||
let peers = if ip_path.exists() { |
||||
std::fs::read_to_string(ip_path) |
||||
.unwrap() |
||||
.split('\n') |
||||
.filter_map(|line| net::SocketAddr::from_str(line).ok()) |
||||
.map(|addr| (addr.ip(), addr.port())) |
||||
.collect() |
||||
} else { |
||||
println!("Warning: there are no peers in IP, which makes it essentially useless"); |
||||
vec![] |
||||
}; |
||||
IPInterface::new(DEFAULT_PORT, peers) |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl IPInterface { |
||||
fn get_connections_to_peers(peers: &[Peer], do_peer_request: bool) -> Vec<TcpStream> { |
||||
peers |
||||
.par_iter() |
||||
.map(Self::new_connection) |
||||
.filter_map(|r| r.ok()) |
||||
.filter_map(|r| r) |
||||
.map(|mut c| -> IFResult<TcpStream> { |
||||
println!("Requesting peers from {:?}", c.peer_addr().unwrap()); |
||||
if do_peer_request { |
||||
Self::request_peers(&mut c)?; |
||||
} |
||||
Ok(c) |
||||
}) |
||||
.filter_map(|r| r.ok()) |
||||
.collect::<Vec<_>>() |
||||
} |
||||
|
||||
pub fn new(port: u16, peers: Vec<Peer>) -> IFResult<Self> { |
||||
let listener = match create_tcp_listener(port) { |
||||
Some(listener) => listener, |
||||
None => { |
||||
return Err(IFError::General(String::from( |
||||
"Unable to open TCP listener", |
||||
))); |
||||
} |
||||
}; |
||||
|
||||
listener.set_nonblocking(true)?; |
||||
|
||||
let connections = Self::get_connections_to_peers(&peers, true); |
||||
|
||||
Ok(IPInterface { |
||||
id: String::from("IP Interface"), |
||||
connections, |
||||
listener, |
||||
peers, |
||||
package_queue: vec![], |
||||
main_loop_iterations: 0, |
||||
}) |
||||
} |
||||
pub fn dump(&self) -> IFResult<Vec<u8>> { |
||||
Ok(serde_cbor::to_vec(&self.peers)?) |
||||
} |
||||
|
||||
pub fn load(&mut self, data: Vec<u8>) -> IFResult<()> { |
||||
let peers: Vec<Peer> = serde_cbor::from_slice(&data)?; |
||||
self.peers = peers; |
||||
Ok(()) |
||||
} |
||||
|
||||
fn send_package(stream: &mut net::TcpStream, package: IPPackage) -> IFResult<()> { |
||||
stream.set_write_timeout(Some(std::time::Duration::from_millis(700)))?; |
||||
#[cfg(test)] |
||||
stream.set_nonblocking(false)?; |
||||
let mut header: Vec<u8> = vec![package.version, package.package_type.as_u8()]; |
||||
for byte in size_to_bytes(package.size) { |
||||
header.push(byte); |
||||
} |
||||
stream.write_all(&*header)?; |
||||
stream.write_all(&*package.message)?; |
||||
|
||||
Ok(()) |
||||
} |
||||
|
||||
fn initialize_connection(&self, conn: &mut TcpStream) -> IFResult<()> { |
||||
if self.peers.len() < PEER_THRESHOLD { |
||||
Self::request_peers(conn)?; |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
fn request_peers(conn: &mut TcpStream) -> IFResult<()> { |
||||
IPInterface::send_package( |
||||
conn, |
||||
IPPackage { |
||||
version: 0, |
||||
package_type: MessageType::PeerRequest, |
||||
size: 0, |
||||
message: vec![], |
||||
}, |
||||
)?; |
||||
Ok(()) |
||||
} |
||||
|
||||
fn obtain_connection(&mut self, addr: &Peer) -> IFResult<usize> { |
||||
if let Some(pos) = self.connections.iter().position(|con| { |
||||
con.peer_addr().is_ok() && compare_addrs(addr, con.peer_addr().unwrap()) |
||||
}) { |
||||
return Ok(pos); |
||||
} |
||||
if let Some(conn) = Self::new_connection(addr)? { |
||||
self.connections.push(conn); |
||||
Ok(self.connections.len() - 1) |
||||
} else { |
||||
Err(IFError::CouldNotConnect) |
||||
} |
||||
} |
||||
|
||||
fn new_connection(addr: &Peer) -> IFResult<Option<TcpStream>> { |
||||
for port in addr.1..addr.1 + 3 { |
||||
match net::TcpStream::connect_timeout( |
||||
&net::SocketAddr::new(addr.0, port as u16), |
||||
Duration::from_millis(300), |
||||
) { |
||||
Ok(connection) => { |
||||
return Ok(Some(connection)); |
||||
} |
||||
Err(_) => continue, |
||||
}; |
||||
} |
||||
Ok(None) |
||||
} |
||||
} |
||||
|
||||
fn create_tcp_listener(port: u16) -> Option<net::TcpListener> { |
||||
for port in port..port + 5 { |
||||
match net::TcpListener::bind("0.0.0.0:".to_owned() + &port.to_string()) { |
||||
Ok(listener) => return Some(listener), |
||||
Err(_e) => {} |
||||
} |
||||
} |
||||
None |
||||
} |
||||
|
||||
fn parse_header(data: Vec<u8>) -> IFResult<IPPackage> { |
||||
Ok(IPPackage { |
||||
version: data[0], |
||||
package_type: MessageType::from_u8(data[1])?, |
||||
size: bytes_to_size([data[3], data[4], data[5], data[6]]), |
||||
message: vec![], |
||||
}) |
||||
} |
||||
|
||||
fn size_to_bytes(mut a: u32) -> [u8; 4] { |
||||
let mut arr: [u8; 4] = [0, 0, 0, 0]; |
||||
for i in [3, 2, 1, 0] { |
||||
arr[i] = (a % 256) as u8; |
||||
a /= 256; |
||||
} |
||||
arr |
||||
} |
||||
|
||||
fn bytes_to_size(arr: [u8; 4]) -> u32 { |
||||
let mut size = 0; |
||||
for size_byte in &arr { |
||||
size = size * 256 + *size_byte as u32; |
||||
} |
||||
size |
||||
} |
||||
|
||||
#[test] |
||||
fn test_creating_connection() -> IFResult<()> { |
||||
let message = *b"Hello world from ironforest"; |
||||
let original_msg_copy = message; |
||||
|
||||
let mut interface1 = IPInterface::new(50000, vec![])?; |
||||
let mut interface2 = IPInterface::new(50001, vec![])?; |
||||
|
||||
let t2 = std::thread::spawn(move || { |
||||
for _ in 0..30 { |
||||
interface2.main_loop_iteration().unwrap(); |
||||
std::thread::sleep(std::time::Duration::from_millis(150)); |
||||
} |
||||
interface2 |
||||
}); |
||||
let t1 = std::thread::spawn(move || { |
||||
interface1 |
||||
.send(&message, Some(String::from("0.0.0.0:50001"))) |
||||
.unwrap(); |
||||
interface1 |
||||
}); |
||||
let res1 = t1.join(); |
||||
match res1 { |
||||
Ok(_res) => { |
||||
println!("Thread Ok"); |
||||
} |
||||
Err(e) => println!("{:?}", e), |
||||
} |
||||
let res2 = t2.join(); |
||||
match res2 { |
||||
Ok(mut res) => { |
||||
println!("Thread Ok"); |
||||
match res.receive() { |
||||
Ok(tmp) => match tmp { |
||||
Some((message, _metadata)) => { |
||||
println!("Received {:?}", message); |
||||
assert_eq!(message, original_msg_copy) |
||||
} |
||||
None => { |
||||
println!("None"); |
||||
panic!(); |
||||
} |
||||
}, |
||||
Err(e) => println!("{:?}", e), |
||||
} |
||||
} |
||||
Err(e) => println!("{:?}", e), |
||||
} |
||||
Ok(()) |
||||
} |
||||
|
||||
#[cfg(test)] |
||||
pub fn create_test_interfaces(n: usize) -> impl Iterator<Item = IPInterface> { |
||||
let ip_addr = std::net::IpAddr::from_str("0.0.0.0").unwrap(); |
||||
(0..n).map(move |i| { |
||||
IPInterface::new( |
||||
(5000 + 5 * i) as u16, |
||||
// (0..n)
|
||||
// .filter(|j| *j != i)
|
||||
// .map(|j| (ip_addr, (5000 + 5 * j) as u16))
|
||||
// .collect(),
|
||||
vec![(ip_addr, (5000 + 5 * ((i + 1) % n)) as u16)], |
||||
) |
||||
.unwrap() |
||||
}) |
||||
} |
@ -1,6 +1,35 @@
|
||||
#[cfg(feature = "std")] |
||||
pub mod ip; |
||||
|
||||
use crate::interface::Interface; |
||||
use alloc::vec; |
||||
use alloc::vec::Vec; |
||||
use alloc::boxed::Box; |
||||
use alloc::string::String; |
||||
#[cfg(feature = "std")] |
||||
use crate::interfaces::ip::IPInterface; |
||||
use crate::res::IFResult; |
||||
|
||||
pub fn get_interfaces() -> alloc::vec::Vec<alloc::boxed::Box<dyn Interface>> { |
||||
#[cfg(not(feature = "std"))] |
||||
pub fn get_interfaces() -> Vec<Box<dyn Interface>> { |
||||
vec![] |
||||
} |
||||
|
||||
#[cfg(feature = "std")] |
||||
pub fn get_interfaces() -> Vec<Box<dyn Interface>> { |
||||
vec![Box::new(IPInterface::from_dump(Default::default()).unwrap())] |
||||
} |
||||
|
||||
#[cfg(not(feature = "std"))] |
||||
pub fn restore_interfaces(_data: Vec<String>) -> IFResult<Vec<Box<dyn Interface>>> { |
||||
Ok(vec![]) |
||||
} |
||||
|
||||
#[cfg(feature = "std")] |
||||
pub fn restore_interfaces(data: Vec<String>) -> IFResult<Vec<Box<dyn Interface>>> { |
||||
if data.is_empty() { |
||||
Ok(get_interfaces()) |
||||
} else { |
||||
Ok(vec![Box::new(IPInterface::from_dump(data[0].clone())?)]) |
||||
} |
||||
} |
||||
|
@ -1,21 +1,32 @@
|
||||
#![no_std] |
||||
#![allow(dead_code)] |
||||
#![feature(trait_alias)] |
||||
#![feature(never_type)] |
||||
|
||||
#[cfg(feature = "std")] |
||||
extern crate std; |
||||
|
||||
extern crate alloc; |
||||
extern crate rand; |
||||
extern crate rsa; |
||||
extern crate serde; |
||||
extern crate core_error; |
||||
extern crate spin; |
||||
|
||||
mod crypto; |
||||
mod ironforce; |
||||
mod message; |
||||
mod transport; |
||||
mod interface; |
||||
mod interfaces; |
||||
mod res; |
||||
pub mod interface; |
||||
pub mod interfaces; |
||||
pub mod res; |
||||
mod tunnel; |
||||
|
||||
|
||||
pub use ironforce::IronForce; |
||||
pub use message::{Message, MessageType}; |
||||
pub use crypto::{Keys, PublicKey}; |
||||
|
||||
#[cfg(test)] |
||||
mod tests { |
||||
} |
||||
|
Loading…
Reference in new issue