use std::{
cell::RefCell,
sync::atomic::{AtomicUsize, Ordering},
};
use async_std::task;
use futures::{
channel::mpsc::{Receiver, SendError, Sender},
sink::SinkExt,
stream::StreamExt,
};
use iced::{
widget::{button, Column, Text},
Application, Command, Element, Subscription,
};
use log::{error, info};
use koibumi_common::{
boxes::{Boxes, DEFAULT_USER_ID},
param::Params,
};
use koibumi_core::{
address::Address,
message::{self, InvHash},
};
use koibumi_node::{self as node, Command as NodeCommand, Event as BmEvent, Response};
use crate::{
bridge,
config::Config as GuiConfig,
contacts, identities, messages, send, settings, status, style, subscriptions,
tab::{Tab, Tabs},
};
const TITLE: &str = "Koibumi - An Experimental Bitmessage Client";
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum State {
Idle,
Running,
Stopping,
}
pub struct Gui {
params: Params,
boxes: Option<Boxes>,
command_sender: Sender<NodeCommand>,
node_handle: task::JoinHandle<()>,
response_receiver: Receiver<Response>,
bm_event_receiver: RefCell<Option<Receiver<BmEvent>>>,
count: AtomicUsize,
state: State,
config: GuiConfig,
tabs: Tabs,
tab: Tab,
messages_tab: messages::Tab,
send_tab: send::Tab,
identities_tab: identities::Tab,
contacts_tab: contacts::Tab,
subscriptions_tab: subscriptions::Tab,
settings_tab: settings::Tab,
status_tab: status::Tab,
logger: crate::log::Logger,
}
#[derive(Clone, Debug)]
pub enum Message {
StartButtonPushed,
StopSent(Result<(), SendError>),
AbortSent(Result<(), SendError>),
BmEvent(BmEvent),
TabSelected(Tab),
MessagesEntryPressed(InvHash),
MessagesUnreadPressed(Option<InvHash>),
MessagesCopyPressed,
SendSubTabSelected(send::SubTab),
SendMessageSubjectChanged(String),
SendMessageBodyChanged(String),
SendMessagePastePressed,
SendMessagePasteContent(Option<String>),
SendBroadcastSubjectChanged(String),
SendBroadcastBodyChanged(String),
SendBroadcastPastePressed,
SendBroadcastPasteContent(Option<String>),
SendClearPressed,
SendSendPressed,
IdentitiesIdentitySelected(usize),
IdentitiesCopyPressed,
IdentitiesSubscribePressed,
IdentitiesGenerateRandomPressed,
IdentitiesDeterministicPasswordChanged(String),
IdentitiesGenerateDeterministicPressed,
IdentitiesChanPasswordChanged(String),
IdentitiesGenerateChanPressed,
ContactsContactSelected(usize),
ContactsCopyPressed,
ContactsSubscribePressed,
SubscriptionsAddressChanged(String),
SubscriptionsSubscribePressed,
SettingsToggleServerEnabled(bool),
SettingsServerChanged(String),
SettingsToggleSocksEnabled(bool),
SettingsSocksChanged(String),
SettingsToggleSocksAuthEnabled(bool),
SettingsSocksUsernameChanged(String),
SettingsSocksPasswordChanged(String),
SettingsToggleConnectToOnion(bool),
SettingsToggleConnectToIp(bool),
SettingsToggleConnectToMyself(bool),
SettingsUserAgentChanged(String),
SettingsSeedsChanged(String),
SettingsOnionPressed,
SettingsMyselfPressed,
SettingsBootstrapsChanged(String),
SettingsMaxIncomingConnectedChanged(String),
SettingsMaxIncomingEstablishedChanged(String),
SettingsMaxOutgoingInitiatedChanged(String),
SettingsMaxOutgoingEstablishedChanged(String),
SettingsOwnNodesChanged(String),
}
async fn send_stop(mut sender: Sender<NodeCommand>) -> Result<(), SendError> {
sender.send(NodeCommand::Stop).await
}
async fn send_abort(mut sender: Sender<NodeCommand>) -> Result<(), SendError> {
sender.send(NodeCommand::Abort).await
}
impl Gui {
fn handle_msg(&mut self, user_id: Vec<u8>, address: Address, object: message::Object) {
task::block_on(async {
if let Some(boxes) = &mut self.boxes {
let identity = boxes.user().private_identity_by_address(&address);
if identity.is_none() {
error!("identity not found for address: {}", address);
return;
}
let identity = identity.unwrap();
match boxes.manager().insert_msg(user_id, identity, object).await {
Ok(message) => {
let entry = messages::Entry::new(koibumi_box::MessageEntry::from(&message));
self.messages_tab.entries.insert(0, entry);
boxes.increment_unread_count();
}
Err(err) => {
error!("{}", err);
return;
}
}
}
});
}
fn handle_broadcast(&mut self, user_id: Vec<u8>, address: Address, object: message::Object) {
task::block_on(async {
if let Some(boxes) = &mut self.boxes {
match boxes
.manager()
.insert_broadcast(user_id, address, object)
.await
{
Ok(message) => {
let entry = messages::Entry::new(koibumi_box::MessageEntry::from(&message));
self.messages_tab.entries.insert(0, entry);
boxes.increment_unread_count();
}
Err(err) => {
error!("{}", err);
return;
}
}
}
});
}
}
impl Application for Gui {
type Executor = iced::executor::Default;
type Message = Message;
type Theme = iced::Theme;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
let params = Params::new();
koibumi_common::log::init(¶ms).unwrap_or_else(|err| {
println!("Warning: Failed to initialize logger.");
println!("{}", err);
});
let config = koibumi_common::config::load(¶ms).unwrap_or_else(|err| {
error!("Failed to load config file: {}", err);
std::process::exit(1)
});
let mut boxes = match task::block_on(koibumi_common::boxes::prepare(¶ms)) {
Ok(boxes) => Some(boxes),
Err(err) => {
error!("{}", err);
None
}
};
let mut messages_tab = messages::Tab::default();
messages_tab.entries = if let Some(boxes) = &mut boxes {
let entries =
task::block_on(async { boxes.manager().message_list(DEFAULT_USER_ID).await });
if let Err(err) = entries {
error!("{}", err);
Vec::new()
} else {
let mut list = Vec::new();
let mut count = 0;
for entry in entries.unwrap() {
if !entry.read() {
count += 1;
}
list.push(messages::Entry::new(entry));
}
boxes.set_unread_count(count);
list
}
} else {
Vec::new()
};
let (command_sender, response_receiver, node_handle) = node::spawn();
(
Self {
params,
boxes,
command_sender,
node_handle,
response_receiver,
bm_event_receiver: RefCell::new(None),
count: AtomicUsize::new(0),
state: State::Idle,
config: GuiConfig::default(),
tabs: Tabs::default(),
tab: Tab::default(),
messages_tab,
send_tab: send::Tab::default(),
identities_tab: identities::Tab::default(),
contacts_tab: contacts::Tab::default(),
subscriptions_tab: subscriptions::Tab::default(),
settings_tab: settings::Tab::new(&config),
status_tab: status::Tab::default(),
logger: crate::log::Logger::default(),
},
Command::none(),
)
}
fn title(&self) -> String {
if let Some(boxes) = &self.boxes {
let count = boxes.unread_count();
if count > 0 {
format!("({}) {}", count, TITLE)
} else {
TITLE.to_string()
}
} else {
format!("[No boxes] {}", TITLE)
}
}
#[allow(clippy::cognitive_complexity)]
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::StartButtonPushed => match self.state {
State::Idle => {
info!("Start");
self.logger.info("Start");
if self.boxes.is_none() {
error!("No boxes");
self.logger.error("No boxes");
return Command::none();
}
let config = self.settings_tab.create_config();
self.settings_tab.seeds_value = config
.seeds()
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(" ");
self.settings_tab.max_outgoing_initiated_value =
config.max_outgoing_initiated().to_string();
self.settings_tab.max_outgoing_established_value =
config.max_outgoing_established().to_string();
self.settings_tab.own_nodes_value = config
.own_nodes()
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join(" ");
if config != self.settings_tab.config {
self.settings_tab.config = config.clone();
if let Err(err) = koibumi_common::config::save(&self.params, &config) {
error!("{}", err);
self.logger.error("Could not save config file");
}
}
let mut sender = self.command_sender.clone();
let response = task::block_on(async {
let pool = koibumi_common::node::prepare(&self.params).await;
if let Err(err) = pool {
error!("{}", err);
self.logger.error("Could not prepare node");
return None;
}
let pool = pool.unwrap();
let users = vec![self.boxes.as_ref().unwrap().user().clone().into()];
if let Err(err) = sender
.send(NodeCommand::Start(config.into(), pool, users))
.await
{
error!("{}", err);
self.logger.error("Could not start node");
return None;
}
self.response_receiver.next().await
});
if let Some(response) = response {
let Response::Started(receiver) = response;
self.bm_event_receiver.replace(Some(receiver));
} else {
return Command::none();
}
self.state = State::Running;
Command::none()
}
State::Running => {
info!("Stop");
self.logger.info("Stop");
self.state = State::Stopping;
Command::perform(send_stop(self.command_sender.clone()), Message::StopSent)
}
State::Stopping => {
info!("Abort");
self.logger.info("Abort");
Command::perform(send_abort(self.command_sender.clone()), Message::AbortSent)
}
},
Message::StopSent(_) => Command::none(),
Message::AbortSent(_) => Command::none(),
Message::BmEvent(bm_event) => {
match bm_event {
BmEvent::ConnectionCounts {
incoming_initiated,
incoming_connected,
incoming_established,
outgoing_initiated,
outgoing_connected,
outgoing_established,
} => {
self.status_tab.incoming_initiated = incoming_initiated;
self.status_tab.incoming_connected = incoming_connected;
self.status_tab.incoming_established = incoming_established;
self.status_tab.outgoing_initiated = outgoing_initiated;
self.status_tab.outgoing_connected = outgoing_connected;
self.status_tab.outgoing_established = outgoing_established;
}
BmEvent::AddrCount(count) => {
self.status_tab.addr_count = count;
}
BmEvent::Established {
addr,
user_agent,
rating,
} => {
self.status_tab.peers.push(addr.clone());
self.status_tab
.peer_infos
.insert(addr, (user_agent, rating));
}
BmEvent::Disconnected { addr } => {
if let Some(index) = self.status_tab.peers.iter().position(|v| v == &addr) {
self.status_tab.peers.remove(index);
}
}
BmEvent::Objects {
missing,
loaded,
uploaded,
} => {
self.status_tab.missing_objects = missing;
self.status_tab.loaded_objects = loaded;
self.status_tab.uploaded_objects = uploaded;
}
BmEvent::Stopped => {
self.state = State::Idle;
}
BmEvent::Msg {
user_id,
address,
object,
} => {
self.handle_msg(user_id, address, object);
}
BmEvent::Broadcast {
user_id,
address,
object,
} => {
self.handle_broadcast(user_id, address, object);
}
}
Command::none()
}
Message::TabSelected(tab) => {
self.tab = tab;
Command::none()
}
Message::MessagesEntryPressed(hash) => self
.messages_tab
.update(Message::MessagesEntryPressed(hash), &mut self.boxes),
Message::MessagesUnreadPressed(hash) => self
.messages_tab
.update(Message::MessagesUnreadPressed(hash), &mut self.boxes),
Message::MessagesCopyPressed => self.messages_tab.update(message, &mut self.boxes),
Message::SendSubTabSelected(tab) => self.send_tab.update(
Message::SendSubTabSelected(tab),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendMessageSubjectChanged(subject) => self.send_tab.update(
Message::SendMessageSubjectChanged(subject),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendMessageBodyChanged(body) => self.send_tab.update(
Message::SendMessageBodyChanged(body),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendMessagePastePressed => self.send_tab.update(
message,
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendMessagePasteContent(s) => self.send_tab.update(
Message::SendMessagePasteContent(s),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendBroadcastSubjectChanged(subject) => self.send_tab.update(
Message::SendBroadcastSubjectChanged(subject),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendBroadcastBodyChanged(body) => self.send_tab.update(
Message::SendBroadcastBodyChanged(body),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendBroadcastPastePressed => self.send_tab.update(
message,
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendBroadcastPasteContent(s) => self.send_tab.update(
Message::SendBroadcastPasteContent(s),
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendClearPressed => self.send_tab.update(
message,
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::SendSendPressed => self.send_tab.update(
message,
self.state,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesIdentitySelected(i) => self.identities_tab.update(
Message::IdentitiesIdentitySelected(i),
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesCopyPressed => self.identities_tab.update(
message,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesSubscribePressed => self.identities_tab.update(
message,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesGenerateRandomPressed => self.identities_tab.update(
message,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesDeterministicPasswordChanged(password) => {
self.identities_tab.update(
Message::IdentitiesDeterministicPasswordChanged(password),
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
)
}
Message::IdentitiesGenerateDeterministicPressed => self.identities_tab.update(
message,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesChanPasswordChanged(password) => self.identities_tab.update(
Message::IdentitiesChanPasswordChanged(password),
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::IdentitiesGenerateChanPressed => self.identities_tab.update(
message,
&mut self.boxes,
&mut self.command_sender,
&mut self.logger,
),
Message::ContactsContactSelected(i) => self.contacts_tab.update(
Message::ContactsContactSelected(i),
&mut self.boxes,
&mut self.command_sender,
),
Message::ContactsCopyPressed => {
self.contacts_tab
.update(message, &mut self.boxes, &mut self.command_sender)
}
Message::ContactsSubscribePressed => {
self.contacts_tab
.update(message, &mut self.boxes, &mut self.command_sender)
}
Message::SubscriptionsAddressChanged(address) => self.subscriptions_tab.update(
Message::SubscriptionsAddressChanged(address),
&mut self.boxes,
&mut self.command_sender,
),
Message::SubscriptionsSubscribePressed => {
self.subscriptions_tab
.update(message, &mut self.boxes, &mut self.command_sender)
}
Message::SettingsToggleServerEnabled(enabled) => self
.settings_tab
.update(Message::SettingsToggleServerEnabled(enabled), self.state),
Message::SettingsServerChanged(s) => self
.settings_tab
.update(Message::SettingsServerChanged(s), self.state),
Message::SettingsToggleSocksEnabled(enabled) => self
.settings_tab
.update(Message::SettingsToggleSocksEnabled(enabled), self.state),
Message::SettingsSocksChanged(s) => self
.settings_tab
.update(Message::SettingsSocksChanged(s), self.state),
Message::SettingsToggleSocksAuthEnabled(enabled) => self
.settings_tab
.update(Message::SettingsToggleSocksAuthEnabled(enabled), self.state),
Message::SettingsSocksUsernameChanged(s) => self
.settings_tab
.update(Message::SettingsSocksUsernameChanged(s), self.state),
Message::SettingsSocksPasswordChanged(s) => self
.settings_tab
.update(Message::SettingsSocksPasswordChanged(s), self.state),
Message::SettingsToggleConnectToOnion(enabled) => self
.settings_tab
.update(Message::SettingsToggleConnectToOnion(enabled), self.state),
Message::SettingsToggleConnectToIp(enabled) => self
.settings_tab
.update(Message::SettingsToggleConnectToIp(enabled), self.state),
Message::SettingsToggleConnectToMyself(enabled) => self
.settings_tab
.update(Message::SettingsToggleConnectToMyself(enabled), self.state),
Message::SettingsUserAgentChanged(s) => self
.settings_tab
.update(Message::SettingsUserAgentChanged(s), self.state),
Message::SettingsSeedsChanged(s) => self
.settings_tab
.update(Message::SettingsSeedsChanged(s), self.state),
Message::SettingsOnionPressed => self.settings_tab.update(message, self.state),
Message::SettingsMyselfPressed => self.settings_tab.update(message, self.state),
Message::SettingsBootstrapsChanged(s) => self
.settings_tab
.update(Message::SettingsBootstrapsChanged(s), self.state),
Message::SettingsMaxIncomingConnectedChanged(s) => self
.settings_tab
.update(Message::SettingsMaxIncomingConnectedChanged(s), self.state),
Message::SettingsMaxIncomingEstablishedChanged(s) => self.settings_tab.update(
Message::SettingsMaxIncomingEstablishedChanged(s),
self.state,
),
Message::SettingsMaxOutgoingInitiatedChanged(s) => self
.settings_tab
.update(Message::SettingsMaxOutgoingInitiatedChanged(s), self.state),
Message::SettingsMaxOutgoingEstablishedChanged(s) => self.settings_tab.update(
Message::SettingsMaxOutgoingEstablishedChanged(s),
self.state,
),
Message::SettingsOwnNodesChanged(s) => self
.settings_tab
.update(Message::SettingsOwnNodesChanged(s), self.state),
}
}
fn view(&self) -> Element<'_, Message, iced::Renderer<Self::Theme>> {
let text_size = self.config.text_size();
let button = |label, style| {
button(Text::new(label).size(text_size))
.padding(text_size / 2)
.style(style)
};
let start_button = {
let (label, color) = match self.state {
State::Idle => (
"Start",
iced::theme::Button::Custom(Box::new(style::StartButton::Start)),
),
State::Running => (
"Stop",
iced::theme::Button::Custom(Box::new(style::StartButton::Stop)),
),
State::Stopping => (
"Abort",
iced::theme::Button::Custom(Box::new(style::StartButton::Abort)),
),
};
button(label, color).on_press(Message::StartButtonPushed)
};
let column = Column::new()
.padding(text_size / 4)
.spacing(text_size / 4)
.push(start_button)
.push(self.tabs.view(&self.config, self.tab))
.push(match self.tab {
Tab::Messages => self.messages_tab.view(&self.config, &self.boxes),
Tab::Send => self.send_tab.view(&self.config, self.state, &self.boxes),
Tab::Identities => {
let identities = if let Some(boxes) = &self.boxes {
boxes.user().private_identities().to_vec()
} else {
Vec::new()
};
let selected_index = if let Some(boxes) = &self.boxes {
boxes.selected_identity_index()
} else {
None
};
self.identities_tab
.view(&self.config, &identities, selected_index, &self.boxes)
}
Tab::Contacts => {
let contacts = if let Some(boxes) = &self.boxes {
boxes.user().contacts().to_vec()
} else {
Vec::new()
};
let selected_index = if let Some(boxes) = &self.boxes {
boxes.selected_contact_index()
} else {
None
};
self.contacts_tab
.view(&self.config, &contacts, selected_index, &self.boxes)
}
Tab::Subscriptions => {
let subscriptions = if let Some(boxes) = &self.boxes {
boxes.user().subscriptions().to_vec()
} else {
Vec::new()
};
self.subscriptions_tab
.view(&self.config, &subscriptions, &self.boxes)
}
Tab::Settings => self.settings_tab.view(&self.config),
Tab::Status => self.status_tab.view(&self.config),
Tab::Log => self.logger.tab.view(&self.config),
});
column.push(self.logger.bar.view(&self.config)).into()
}
fn subscription(&self) -> Subscription<Message> {
match self.state {
State::Idle => Subscription::none(),
State::Running | State::Stopping => {
if let Some(receiver) = self.bm_event_receiver.replace(None) {
let id = self.count.fetch_add(1, Ordering::SeqCst);
bridge::bridge(receiver, id).map(Message::BmEvent)
} else {
let id = self.count.load(Ordering::SeqCst) - 1;
bridge::keep_alive(id).map(Message::BmEvent)
}
}
}
}
}