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 crate::interface::Interface; |
||||||
use alloc::vec; |
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![] |
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] |
#![no_std] |
||||||
#![allow(dead_code)] |
#![allow(dead_code)] |
||||||
|
#![feature(trait_alias)] |
||||||
|
#![feature(never_type)] |
||||||
|
|
||||||
|
#[cfg(feature = "std")] |
||||||
|
extern crate std; |
||||||
|
|
||||||
extern crate alloc; |
extern crate alloc; |
||||||
extern crate rand; |
extern crate rand; |
||||||
extern crate rsa; |
extern crate rsa; |
||||||
extern crate serde; |
extern crate serde; |
||||||
extern crate core_error; |
extern crate core_error; |
||||||
|
extern crate spin; |
||||||
|
|
||||||
mod crypto; |
mod crypto; |
||||||
mod ironforce; |
mod ironforce; |
||||||
mod message; |
mod message; |
||||||
mod transport; |
mod transport; |
||||||
mod interface; |
pub mod interface; |
||||||
mod interfaces; |
pub mod interfaces; |
||||||
mod res; |
pub mod res; |
||||||
mod tunnel; |
mod tunnel; |
||||||
|
|
||||||
|
|
||||||
|
pub use ironforce::IronForce; |
||||||
|
pub use message::{Message, MessageType}; |
||||||
|
pub use crypto::{Keys, PublicKey}; |
||||||
|
|
||||||
#[cfg(test)] |
#[cfg(test)] |
||||||
mod tests { |
mod tests { |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue