Lev
3 years ago
14 changed files with 628 additions and 430 deletions
@ -1,26 +1 @@ |
|||||||
use crate::message::{DegMessage, Profile}; |
pub use degeon_core::{GuiEvent, AppScreen}; |
||||||
use ironforce::PublicKey; |
|
||||||
use crate::state::AppScreen; |
|
||||||
|
|
||||||
/// An enum with all possible events for this application
|
|
||||||
#[derive(Clone, Debug)] |
|
||||||
pub enum GuiEvent { |
|
||||||
/// Selection of a chat
|
|
||||||
ChatSelect(usize), |
|
||||||
/// The user changed the value of "new message" field
|
|
||||||
Typed(String), |
|
||||||
/// The user clicked "Send"
|
|
||||||
SendMessage, |
|
||||||
/// A new messaged arrived
|
|
||||||
NewMessageInChat(PublicKey, DegMessage), |
|
||||||
/// A profile response arrived and we should store it
|
|
||||||
SetProfile(PublicKey, Profile), |
|
||||||
/// We should send profile (in response to profile request)
|
|
||||||
WeHaveToSendProfile(PublicKey), |
|
||||||
/// Go to another screen
|
|
||||||
ChangeScreen(AppScreen), |
|
||||||
/// Changed the name in the field on profile screen
|
|
||||||
ChangeName(String), |
|
||||||
/// Nothing happened
|
|
||||||
None, |
|
||||||
} |
|
||||||
|
@ -1,48 +1 @@ |
|||||||
use ironforce::PublicKey; |
pub use degeon_core::{ProtocolMsg, Profile, DegMessage, DegMessageContent}; |
||||||
use serde::{Deserialize, Serialize}; |
|
||||||
|
|
||||||
/// A message in the messenger
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
|
||||||
pub struct DegMessage { |
|
||||||
pub sender: PublicKey, |
|
||||||
pub timestamp: i64, |
|
||||||
pub content: DegMessageContent, |
|
||||||
} |
|
||||||
|
|
||||||
impl DegMessage { |
|
||||||
/// Create a simple text message
|
|
||||||
pub fn new_text(text: String, my_key: &PublicKey) -> DegMessage { |
|
||||||
Self { |
|
||||||
sender: my_key.clone(), |
|
||||||
timestamp: chrono::Utc::now().timestamp(), |
|
||||||
content: DegMessageContent::Text(text), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// The content of the message
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
|
||||||
pub enum DegMessageContent { |
|
||||||
Text(String), |
|
||||||
File(Vec<u8>), |
|
||||||
Service, |
|
||||||
} |
|
||||||
|
|
||||||
/// User's profile
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, Default)] |
|
||||||
pub struct Profile { |
|
||||||
pub name: String, |
|
||||||
} |
|
||||||
|
|
||||||
/// A protocol message (that's sent through IF)
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)] |
|
||||||
pub enum ProtocolMsg { |
|
||||||
/// Requesting profile
|
|
||||||
ProfileRequest, |
|
||||||
/// Responding to the profile request with a profile
|
|
||||||
ProfileResponse(Profile), |
|
||||||
/// A peer discovery
|
|
||||||
Ping, |
|
||||||
/// A message is sent
|
|
||||||
NewMessage(DegMessage), |
|
||||||
} |
|
||||||
|
@ -0,0 +1,25 @@ |
|||||||
|
[package] |
||||||
|
name = "degeon_core" |
||||||
|
version = "0.1.0" |
||||||
|
edition = "2021" |
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||||
|
|
||||||
|
[dependencies] |
||||||
|
ironforce = { path = "..", features = ["std"] } |
||||||
|
base64 = "0.13.0" |
||||||
|
serde = { version = "1.0" } |
||||||
|
serde_json = "1.0.72" |
||||||
|
futures = "0.3.18" |
||||||
|
chrono = "0.4.19" |
||||||
|
iced = { version = "0.3.0", features = ["glow"] } |
||||||
|
|
||||||
|
[dependencies.pyo3] |
||||||
|
version = "0.14.0" |
||||||
|
features = ["extension-module"] |
||||||
|
|
||||||
|
[lib] |
||||||
|
name = "degeon_core" |
||||||
|
path = "src/lib.rs" |
||||||
|
crate-type = ["cdylib", "rlib"] |
||||||
|
|
@ -0,0 +1,56 @@ |
|||||||
|
use ironforce::{Keys, PublicKey}; |
||||||
|
use crate::message::{DegMessage, DegMessageContent, Profile}; |
||||||
|
use serde::{Serialize, Deserialize}; |
||||||
|
|
||||||
|
|
||||||
|
/// A chat in the messenger
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||||
|
pub struct Chat { |
||||||
|
/// Public key of the other user
|
||||||
|
pub pkey: PublicKey, |
||||||
|
/// Messages in this chat
|
||||||
|
pub messages: Vec<DegMessage>, |
||||||
|
/// Profile of the other user
|
||||||
|
pub profile: Profile, |
||||||
|
/// Scroll position
|
||||||
|
pub scrolled: f32, |
||||||
|
/// Message in the field
|
||||||
|
pub input: String, |
||||||
|
} |
||||||
|
|
||||||
|
impl Chat { |
||||||
|
/// Create a new chat
|
||||||
|
pub fn new(pkey: PublicKey) -> Self { |
||||||
|
Self { |
||||||
|
pkey, |
||||||
|
messages: vec![], |
||||||
|
profile: Profile { name: "".to_string() }, |
||||||
|
scrolled: 0.0, |
||||||
|
input: "".to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// Create an example chat
|
||||||
|
pub fn example(i: usize, my_keys: &Keys) -> Chat { |
||||||
|
let pkey = Keys::generate().get_public(); |
||||||
|
Self { |
||||||
|
messages: vec![ |
||||||
|
DegMessage { |
||||||
|
sender: pkey.clone(), |
||||||
|
timestamp: chrono::Utc::now().timestamp(), |
||||||
|
content: DegMessageContent::Text(format!("Example message {}", 2 * i)) |
||||||
|
}, |
||||||
|
DegMessage { |
||||||
|
sender: my_keys.get_public(), |
||||||
|
timestamp: chrono::Utc::now().timestamp(), |
||||||
|
content: DegMessageContent::Text(format!("Example message {}", 2 * i + 1)) |
||||||
|
}, |
||||||
|
|
||||||
|
], |
||||||
|
profile: Profile { name: format!("Example user ({})", i) }, |
||||||
|
scrolled: 0.0, |
||||||
|
pkey, |
||||||
|
input: "".to_string(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,257 @@ |
|||||||
|
use crate::chat::Chat; |
||||||
|
use crate::gui_events::GuiEvent; |
||||||
|
use crate::message::{Profile, ProtocolMsg}; |
||||||
|
use futures::Stream; |
||||||
|
use ironforce::res::IFResult; |
||||||
|
use ironforce::{IronForce, Keys, Message, MessageType, PublicKey}; |
||||||
|
use std::pin::Pin; |
||||||
|
use std::sync::{Arc, Mutex}; |
||||||
|
use std::task::{Context, Poll}; |
||||||
|
use pyo3::prelude::*; |
||||||
|
use serde::{Serialize, Deserialize}; |
||||||
|
|
||||||
|
/// The container for logic, data, IF and protocol interactions
|
||||||
|
#[derive(Clone)] |
||||||
|
#[pyclass] |
||||||
|
pub struct Degeon { |
||||||
|
/// The list of all chats for this instance
|
||||||
|
pub chats: Vec<Chat>, |
||||||
|
/// Profile of this user
|
||||||
|
pub profile: Profile, |
||||||
|
/// Keys of this user
|
||||||
|
pub keys: Keys, |
||||||
|
/// The IF worker
|
||||||
|
pub ironforce: Arc<Mutex<IronForce>>, |
||||||
|
} |
||||||
|
|
||||||
|
/// Data for serialization
|
||||||
|
#[derive(Serialize, Deserialize)] |
||||||
|
pub struct DegeonData { |
||||||
|
pub chats: Vec<Chat>, |
||||||
|
pub profile: Profile, |
||||||
|
pub keys: Keys, |
||||||
|
} |
||||||
|
|
||||||
|
/// Load IF and launch the main loop
|
||||||
|
///
|
||||||
|
/// Returns ironforce and keys
|
||||||
|
fn get_initialized_ironforce() -> (Arc<Mutex<IronForce>>, Keys) { |
||||||
|
let ironforce = IronForce::from_file("".to_string()).unwrap(); |
||||||
|
let keys = ironforce.keys.clone(); |
||||||
|
println!("ID: {}", keys.get_public().get_short_id()); |
||||||
|
let (_thread, ironforce) = ironforce.launch_main_loop(1000); |
||||||
|
(ironforce, keys) |
||||||
|
} |
||||||
|
|
||||||
|
impl Default for Degeon { |
||||||
|
fn default() -> Self { |
||||||
|
let (ironforce, keys) = get_initialized_ironforce(); |
||||||
|
Self { |
||||||
|
chats: vec![], |
||||||
|
profile: Profile::default(), |
||||||
|
keys, |
||||||
|
ironforce, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Degeon { |
||||||
|
/// Get profile for the current user
|
||||||
|
pub fn get_profile(&self) -> Profile { |
||||||
|
self.profile.clone() |
||||||
|
} |
||||||
|
|
||||||
|
/// Find a chat in the list for a given public key
|
||||||
|
pub fn chat_with(&self, pkey: &PublicKey) -> Option<usize> { |
||||||
|
self.chats.iter().position(|chat| &chat.pkey == pkey) |
||||||
|
} |
||||||
|
|
||||||
|
/// Process the incoming message and act accordingly
|
||||||
|
pub fn process_message(&self, msg: ironforce::Message) -> IFResult<Option<GuiEvent>> { |
||||||
|
let deg_msg: ProtocolMsg = |
||||||
|
match serde_json::from_slice(msg.get_decrypted(&self.keys)?.as_slice()) { |
||||||
|
Ok(r) => r, |
||||||
|
Err(_) => return Ok(None), |
||||||
|
}; |
||||||
|
let sender = msg.get_sender(&self.keys).unwrap(); |
||||||
|
println!( |
||||||
|
"check_rec: {:?}, sender==self: {:?}", |
||||||
|
msg.check_recipient(&self.keys), |
||||||
|
sender == self.keys.get_public() |
||||||
|
); |
||||||
|
if !msg.check_recipient(&self.keys) || sender == self.keys.get_public() { |
||||||
|
return Ok(None); |
||||||
|
} |
||||||
|
println!("{:?}", deg_msg); |
||||||
|
Ok(match °_msg { |
||||||
|
ProtocolMsg::NewMessage(deg_msg) => { |
||||||
|
Some(GuiEvent::NewMessageInChat(sender, deg_msg.clone())) |
||||||
|
} |
||||||
|
ProtocolMsg::ProfileRequest | ProtocolMsg::Ping => { |
||||||
|
Some(GuiEvent::WeHaveToSendProfile(sender)) |
||||||
|
} |
||||||
|
ProtocolMsg::ProfileResponse(prof) => Some(GuiEvent::SetProfile(sender, prof.clone())), |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/// Send a multicast message through the network
|
||||||
|
pub fn send_multicast(&self, msg: ProtocolMsg) -> IFResult<()> { |
||||||
|
self.ironforce.lock().unwrap().send_to_all( |
||||||
|
Message::build() |
||||||
|
.message_type(MessageType::Broadcast) |
||||||
|
.content(serde_json::to_vec(&msg)?) |
||||||
|
.sign(&self.keys) |
||||||
|
.build()?, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// Send a message to a target through the network
|
||||||
|
pub fn send_message(&self, msg: ProtocolMsg, target: &PublicKey) -> IFResult<()> { |
||||||
|
// if self.ironforce.lock().unwrap().get_tunnel(target).is_none() {
|
||||||
|
// println!("Creating a tunnel");
|
||||||
|
// self.ironforce
|
||||||
|
// .lock()
|
||||||
|
// .unwrap()
|
||||||
|
// .initialize_tunnel_creation(target)?;
|
||||||
|
// let mut counter = 0;
|
||||||
|
// while self.ironforce.lock().unwrap().get_tunnel(target).is_none() {
|
||||||
|
// std::thread::sleep(std::time::Duration::from_millis(350));
|
||||||
|
// counter += 1;
|
||||||
|
// if counter > 100 {
|
||||||
|
// return Err(IFError::TunnelNotFound);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
self.ironforce.lock().unwrap().send_to_all( |
||||||
|
Message::build() |
||||||
|
.message_type(MessageType::Broadcast) |
||||||
|
.content(serde_json::to_vec(&msg)?) |
||||||
|
.recipient(target) |
||||||
|
.sign(&self.keys) |
||||||
|
.build()?, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/// Created an iced command that sends a message to a target
|
||||||
|
pub fn get_send_command( |
||||||
|
&self, |
||||||
|
msg: ProtocolMsg, |
||||||
|
target: &PublicKey, |
||||||
|
) -> iced::Command<GuiEvent> { |
||||||
|
let if_clone = self.ironforce.clone(); |
||||||
|
let _target = target.clone(); |
||||||
|
let keys = self.keys.clone(); |
||||||
|
|
||||||
|
println!("Creating a send command: {:?}", msg); |
||||||
|
iced::Command::perform(async {}, move |_| { |
||||||
|
println!("Sending message: {:?}", msg); |
||||||
|
if_clone |
||||||
|
.lock() |
||||||
|
.unwrap() |
||||||
|
.send_to_all( |
||||||
|
Message::build() |
||||||
|
.message_type(MessageType::Broadcast) |
||||||
|
.content(serde_json::to_vec(&msg).unwrap()) |
||||||
|
// todo:
|
||||||
|
// .recipient(&target)
|
||||||
|
.sign(&keys) |
||||||
|
.build() |
||||||
|
.unwrap(), |
||||||
|
) |
||||||
|
.unwrap(); |
||||||
|
GuiEvent::None |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
pub const DEFAULT_FILENAME: &str = ".degeon.json"; |
||||||
|
|
||||||
|
impl Degeon { |
||||||
|
/// Store most of the necessary data to string
|
||||||
|
pub fn serialize_to_string(&self) -> serde_json::Result<String> { |
||||||
|
let data = DegeonData { |
||||||
|
chats: self.chats.clone(), |
||||||
|
profile: self.get_profile(), |
||||||
|
keys: self.keys.clone(), |
||||||
|
}; |
||||||
|
serde_json::to_string(&data) |
||||||
|
} |
||||||
|
|
||||||
|
/// Restore `Degeon` from serialized data
|
||||||
|
pub fn restore_from_string(data: String) -> IFResult<Self> { |
||||||
|
let data_res: serde_json::Result<DegeonData> = serde_json::from_str(data.as_str()); |
||||||
|
let data = match data_res { |
||||||
|
Ok(r) => r, |
||||||
|
Err(_) => return Ok(Self::default()), |
||||||
|
}; |
||||||
|
let (ironforce, _keys) = get_initialized_ironforce(); |
||||||
|
ironforce.lock().unwrap().keys = data.keys.clone(); |
||||||
|
let deg = Degeon { |
||||||
|
chats: data.chats, |
||||||
|
profile: data.profile, |
||||||
|
keys: data.keys, |
||||||
|
ironforce |
||||||
|
}; |
||||||
|
Ok(deg) |
||||||
|
} |
||||||
|
|
||||||
|
/// Save to a file. If no filename is provided, the default is used
|
||||||
|
pub fn save_to_file(&self, filename: String) -> IFResult<()> { |
||||||
|
let data = self.serialize_to_string()?; |
||||||
|
let filename = if filename.is_empty() { |
||||||
|
DEFAULT_FILENAME.to_string() |
||||||
|
} else { |
||||||
|
filename |
||||||
|
}; |
||||||
|
std::fs::write(filename, data)?; |
||||||
|
Ok(()) |
||||||
|
} |
||||||
|
|
||||||
|
/// Restore from a file. If no filename is provided, the default is used
|
||||||
|
pub fn restore_from_file(filename: String) -> IFResult<Self> { |
||||||
|
let filename = if filename.is_empty() { |
||||||
|
DEFAULT_FILENAME.to_string() |
||||||
|
} else { |
||||||
|
filename |
||||||
|
}; |
||||||
|
let content = std::fs::read_to_string(filename).unwrap_or_default(); |
||||||
|
Self::restore_from_string(content) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
impl Stream for Degeon { |
||||||
|
type Item = GuiEvent; |
||||||
|
|
||||||
|
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { |
||||||
|
let timestamp_0 = std::time::Instant::now(); |
||||||
|
let msg_raw = self.ironforce.lock().unwrap().read_message(); |
||||||
|
let msg = msg_raw |
||||||
|
.as_ref() |
||||||
|
.map(|msg| self.process_message(msg.clone()).unwrap()); |
||||||
|
if msg_raw.is_some() { |
||||||
|
let msg_deg: ProtocolMsg = match serde_json::from_slice( |
||||||
|
msg_raw |
||||||
|
.as_ref() |
||||||
|
.unwrap() |
||||||
|
.get_decrypted(&self.keys) |
||||||
|
.unwrap() |
||||||
|
.as_slice(), |
||||||
|
) { |
||||||
|
Ok(r) => r, |
||||||
|
Err(_) => { |
||||||
|
println!("Couldn't deserialize {:?}", msg_raw); |
||||||
|
return Poll::Ready(Some(GuiEvent::None)); |
||||||
|
} |
||||||
|
}; |
||||||
|
println!("{:?} -> {:?}", msg_deg, msg); |
||||||
|
} |
||||||
|
if timestamp_0.elapsed() < std::time::Duration::from_millis(5) { |
||||||
|
std::thread::sleep(std::time::Duration::from_millis(5)); |
||||||
|
} |
||||||
|
match msg { |
||||||
|
None => Poll::Ready(Some(GuiEvent::None)), |
||||||
|
Some(None) => Poll::Ready(Some(GuiEvent::None)), |
||||||
|
Some(Some(msg)) => Poll::Ready(Some(msg)), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
use crate::message::{DegMessage, Profile}; |
||||||
|
use ironforce::PublicKey; |
||||||
|
use serde::{Serialize, Deserialize}; |
||||||
|
|
||||||
|
|
||||||
|
/// The screens (pages) of the app
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] |
||||||
|
pub enum AppScreen { |
||||||
|
/// The one with the chats
|
||||||
|
Main, |
||||||
|
/// Settings and profile
|
||||||
|
ProfileEditor, |
||||||
|
/// The screen that appears if no peers are available and asks for the IP address of at least one peer
|
||||||
|
#[allow(dead_code)] |
||||||
|
PeerInput, |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
impl Default for AppScreen { |
||||||
|
fn default() -> Self { |
||||||
|
AppScreen::Main |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// An enum with all possible events for this application
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||||
|
pub enum GuiEvent { |
||||||
|
/// Selection of a chat
|
||||||
|
ChatSelect(usize), |
||||||
|
/// The user changed the value of "new message" field
|
||||||
|
Typed(String), |
||||||
|
/// The user clicked "Send"
|
||||||
|
SendMessage, |
||||||
|
/// A new messaged arrived
|
||||||
|
NewMessageInChat(PublicKey, DegMessage), |
||||||
|
/// A profile response arrived and we should store it
|
||||||
|
SetProfile(PublicKey, Profile), |
||||||
|
/// We should send profile (in response to profile request)
|
||||||
|
WeHaveToSendProfile(PublicKey), |
||||||
|
/// Go to another screen
|
||||||
|
ChangeScreen(AppScreen), |
||||||
|
/// Changed the name in the field on profile screen
|
||||||
|
ChangeName(String), |
||||||
|
/// Nothing happened
|
||||||
|
None, |
||||||
|
} |
||||||
|
|
||||||
|
impl GuiEvent { |
||||||
|
pub fn get_json(&self) -> String { |
||||||
|
serde_json::to_string(&self).unwrap() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
extern crate serde; |
||||||
|
extern crate pyo3; |
||||||
|
mod chat; |
||||||
|
mod degeon_worker; |
||||||
|
mod gui_events; |
||||||
|
mod message; |
||||||
|
|
||||||
|
pub use chat::Chat; |
||||||
|
pub use degeon_worker::{Degeon, DegeonData, DEFAULT_FILENAME}; |
||||||
|
pub use message::{DegMessage, Profile, ProtocolMsg, DegMessageContent}; |
||||||
|
pub use gui_events::{AppScreen, GuiEvent}; |
||||||
|
use pyo3::prelude::*; |
||||||
|
use pyo3::wrap_pyfunction; |
||||||
|
|
||||||
|
#[pyfunction] |
||||||
|
fn new_degeon() -> PyResult<Degeon> { |
||||||
|
Ok(Degeon::default()) |
||||||
|
} |
||||||
|
|
||||||
|
#[pymodule] |
||||||
|
fn degeon_core(_py: Python, m: &PyModule) -> PyResult<()> { |
||||||
|
m.add_class::<Degeon>()?; |
||||||
|
m.add_function(wrap_pyfunction!(new_degeon, m)?)?; |
||||||
|
Ok(()) |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
use ironforce::PublicKey; |
||||||
|
use serde::{Deserialize, Serialize}; |
||||||
|
|
||||||
|
/// A message in the messenger
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||||
|
pub struct DegMessage { |
||||||
|
pub sender: PublicKey, |
||||||
|
pub timestamp: i64, |
||||||
|
pub content: DegMessageContent, |
||||||
|
} |
||||||
|
|
||||||
|
impl DegMessage { |
||||||
|
/// Create a simple text message
|
||||||
|
pub fn new_text(text: String, my_key: &PublicKey) -> DegMessage { |
||||||
|
Self { |
||||||
|
sender: my_key.clone(), |
||||||
|
timestamp: chrono::Utc::now().timestamp(), |
||||||
|
content: DegMessageContent::Text(text), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// The content of the message
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||||
|
pub enum DegMessageContent { |
||||||
|
Text(String), |
||||||
|
File(Vec<u8>), |
||||||
|
Service, |
||||||
|
} |
||||||
|
|
||||||
|
/// User's profile
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)] |
||||||
|
pub struct Profile { |
||||||
|
pub name: String, |
||||||
|
} |
||||||
|
|
||||||
|
/// A protocol message (that's sent through IF)
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)] |
||||||
|
pub enum ProtocolMsg { |
||||||
|
/// Requesting profile
|
||||||
|
ProfileRequest, |
||||||
|
/// Responding to the profile request with a profile
|
||||||
|
ProfileResponse(Profile), |
||||||
|
/// A peer discovery
|
||||||
|
Ping, |
||||||
|
/// A message is sent
|
||||||
|
NewMessage(DegMessage), |
||||||
|
} |
Loading…
Reference in new issue