koibumi 0.0.9

An experimental Bitmessage client
use async_std::task;
use copypasta::ClipboardProvider;
use futures::{channel::mpsc::Sender, sink::SinkExt};
use iced::{
    button, scrollable, text_input, Button, Color, Column, Command, Element, Length, Row,
    Scrollable, Text, TextInput,
};
use log::{debug, error};
use rand::Rng;

use koibumi_common::boxes::Boxes;
use koibumi_core::{
    content, crypto, encoding,
    identity::{Private as PrivateIdentity, Public as PublicIdentity},
    io::WriteTo,
    object,
    time::Time,
};
use koibumi_node::Command as NodeCommand;

use crate::{config::Config as GuiConfig, gui, log::Logger, style};

#[derive(Clone, Debug)]
pub enum Message {
    SubTabSelected(SubTab),

    MessageSubjectChanged(String),
    MessageBodyChanged(String),
    MessagePastePressed,

    BroadcastSubjectChanged(String),
    BroadcastBodyChanged(String),
    BroadcastPastePressed,

    ClearPressed,
    SendPressed,
}

#[derive(Clone, Debug, Default)]
struct MessageSubTab {
    subject: text_input::State,
    subject_value: String,
    body: text_input::State,
    body_value: String,
    paste_button: button::State,
    body_scroll: scrollable::State,
}

#[derive(Clone, Debug, Default)]
struct BroadcastSubTab {
    subject: text_input::State,
    subject_value: String,
    body: text_input::State,
    body_value: String,
    paste_button: button::State,
    body_scroll: scrollable::State,
}

#[derive(Clone, Debug, Default)]
pub(crate) struct Tab {
    sub_tabs: SubTabs,
    sub_tab: SubTab,

    message_sub_tab: MessageSubTab,
    broadcast_sub_tab: BroadcastSubTab,

    clear_button: button::State,
    send_button: button::State,
}

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SubTab {
    Message,
    Broadcast,
}

impl Default for SubTab {
    fn default() -> Self {
        Self::Message
    }
}

#[derive(Clone, Debug, Default)]
struct SubTabs {
    message_button: button::State,
    broadcast_button: button::State,
}

impl SubTabs {
    pub(crate) fn view(&mut self, config: &GuiConfig, current_tab: SubTab) -> Row<gui::Message> {
        let text_size = config.text_size();

        let tab_button = |state, label, tab| {
            Button::new(state, Text::new(label).size(text_size))
                .style(style::TabButton::new(tab == current_tab))
                .on_press(gui::Message::SendMessage(Message::SubTabSelected(tab)))
                .padding(text_size / 6)
        };

        Row::new().spacing(text_size / 4).push(
            Row::new()
                .width(Length::Shrink)
                .spacing(text_size / 4)
                .push(tab_button(
                    &mut self.message_button,
                    "Send ordinary Message",
                    SubTab::Message,
                ))
                .push(tab_button(
                    &mut self.broadcast_button,
                    "Send Message to your Subscribers",
                    SubTab::Broadcast,
                )),
        )
    }
}

const RANDOM_TTL_SIZE: u64 = 600;

fn send_msg(
    command_sender: &mut Sender<NodeCommand>,
    from_identity: &PrivateIdentity,
    to_identity: &PublicIdentity,
    subject: &str,
    body: &str,
    logger: &mut Logger,
) {
    debug!("Start send_msg");

    let simple = encoding::Simple::new(subject.as_bytes().to_vec(), body.as_bytes().to_vec());
    if let Err(err) = simple {
        error!("{}", err);
        return;
    }
    let simple = simple.unwrap();
    let mut simple_bytes = Vec::new();
    if let Err(err) = simple.write_to(&mut simple_bytes) {
        error!("{}", err);
        return;
    }

    let rand_ttl = rand::thread_rng().gen_range(0, RANDOM_TTL_SIZE);
    let expires_time =
        (Time::now().as_secs() + 60 * 60 * 24 * 2 + rand_ttl - RANDOM_TTL_SIZE / 2).into();
    let object_type = object::ObjectKind::Msg.into();
    let version = 1.into();
    let stream_number = from_identity.address().stream();
    let header = object::Header::new(expires_time, object_type, version, stream_number);

    let mut header_bytes = Vec::new();
    header.write_to(&mut header_bytes).unwrap();

    let msg = content::Msg::new(
        &header_bytes,
        &from_identity,
        &to_identity,
        encoding::Encoding::Simple,
        simple_bytes,
    );
    if let Err(err) = msg {
        error!("{}", err);
        return;
    }
    let msg = msg.unwrap();
    let mut msg_bytes = Vec::new();
    msg.write_to(&mut msg_bytes).unwrap();
    let public_key = to_identity.public_encryption_key();
    let encrypted = crypto::Encrypted::encrypt(msg_bytes, &public_key);
    if let Err(err) = encrypted {
        error!("{}", err);
        return;
    }
    let encrypted = encrypted.unwrap();
    let mut encrypted_bytes = Vec::new();
    encrypted.write_to(&mut encrypted_bytes).unwrap();
    let msg = object::Msg::new(encrypted_bytes);
    let mut payload = Vec::new();
    msg.write_to(&mut payload).unwrap();

    if let Err(err) = task::block_on(command_sender.send(NodeCommand::Send { header, payload })) {
        error!("{}", err);
    }

    debug!("End send_msg");
    logger.info("Message sent");
}

fn send_broadcast(
    command_sender: &mut Sender<NodeCommand>,
    from_identity: &PrivateIdentity,
    subject: &str,
    body: &str,
    logger: &mut Logger,
) {
    debug!("Start send_broadcast");

    let simple = encoding::Simple::new(subject.as_bytes().to_vec(), body.as_bytes().to_vec());
    if let Err(err) = simple {
        error!("{}", err);
        return;
    }
    let simple = simple.unwrap();
    let mut simple_bytes = Vec::new();
    if let Err(err) = simple.write_to(&mut simple_bytes) {
        error!("{}", err);
        return;
    }

    let rand_ttl = rand::thread_rng().gen_range(0, RANDOM_TTL_SIZE);
    let expires_time =
        (Time::now().as_secs() + 60 * 60 * 24 * 2 + rand_ttl - RANDOM_TTL_SIZE / 2).into();
    let object_type = object::ObjectKind::Broadcast.into();
    let version = 5.into();
    let stream_number = from_identity.address().stream();
    let header = object::Header::new(expires_time, object_type, version, stream_number);

    let mut header_bytes = Vec::new();
    header.write_to(&mut header_bytes).unwrap();
    if version.as_u64() == 5 {
        from_identity
            .address()
            .broadcast_tag()
            .write_to(&mut header_bytes)
            .unwrap();
    }

    let broadcast = content::Broadcast::new(
        &header_bytes,
        &from_identity,
        encoding::Encoding::Simple,
        simple_bytes,
    );
    if let Err(err) = broadcast {
        error!("{}", err);
        return;
    }
    let broadcast = broadcast.unwrap();
    let mut broadcast_bytes = Vec::new();
    broadcast.write_to(&mut broadcast_bytes).unwrap();
    let private_key = from_identity.address().broadcast_private_encryption_key();
    if let Err(err) = private_key {
        error!("{}", err);
        return;
    }
    let private_key = private_key.unwrap();
    let encrypted = crypto::Encrypted::encrypt(broadcast_bytes, &private_key.public_key());
    if let Err(err) = encrypted {
        error!("{}", err);
        return;
    }
    let encrypted = encrypted.unwrap();
    let mut encrypted_bytes = Vec::new();
    encrypted.write_to(&mut encrypted_bytes).unwrap();
    let broadcast_v5 =
        object::BroadcastV5::new(from_identity.address().broadcast_tag(), encrypted_bytes);
    let mut payload = Vec::new();
    broadcast_v5.write_to(&mut payload).unwrap();

    if let Err(err) = task::block_on(command_sender.send(NodeCommand::Send { header, payload })) {
        error!("{}", err);
    }

    debug!("End send_broadcast");
    logger.info("Broadcast sent");
}

impl Tab {
    pub(crate) fn update(
        &mut self,
        message: Message,
        state: gui::State,
        boxes: &mut Option<Boxes>,
        command_sender: &mut Sender<NodeCommand>,
        logger: &mut Logger,
    ) -> Command<gui::Message> {
        match message {
            Message::SubTabSelected(sub_tab) => {
                self.sub_tab = sub_tab;
                Command::none()
            }

            Message::MessageSubjectChanged(s) => {
                self.message_sub_tab.subject_value = s;
                Command::none()
            }
            Message::MessageBodyChanged(s) => {
                self.message_sub_tab.body_value = s;
                Command::none()
            }
            Message::MessagePastePressed => {
                let ctx = copypasta::ClipboardContext::new();
                if let Err(err) = ctx {
                    error!("{}", err);
                    return Command::none();
                }
                let mut ctx = ctx.unwrap();
                let content = ctx.get_contents();
                if let Err(err) = content {
                    error!("{}", err);
                    return Command::none();
                }
                self.message_sub_tab.body_value = content.unwrap();
                Command::none()
            }

            Message::BroadcastSubjectChanged(s) => {
                self.broadcast_sub_tab.subject_value = s;
                Command::none()
            }
            Message::BroadcastBodyChanged(s) => {
                self.broadcast_sub_tab.body_value = s;
                Command::none()
            }
            Message::BroadcastPastePressed => {
                let ctx = copypasta::ClipboardContext::new();
                if let Err(err) = ctx {
                    error!("{}", err);
                    return Command::none();
                }
                let mut ctx = ctx.unwrap();
                let content = ctx.get_contents();
                if let Err(err) = content {
                    error!("{}", err);
                    return Command::none();
                }
                self.broadcast_sub_tab.body_value = content.unwrap();
                Command::none()
            }

            Message::ClearPressed => {
                self.message_sub_tab.subject_value = String::new();
                self.message_sub_tab.body_value = String::new();
                self.broadcast_sub_tab.subject_value = String::new();
                self.broadcast_sub_tab.body_value = String::new();
                Command::none()
            }
            Message::SendPressed => {
                if state != gui::State::Running {
                    logger.error("Run node before send message");
                    return Command::none();
                }

                if boxes.is_none() {
                    return Command::none();
                }
                let boxes = boxes.as_mut().unwrap();

                if boxes.selected_identity_index().is_none() {
                    return Command::none();
                }
                let index = boxes.selected_identity_index().unwrap();
                let from_identity = &boxes.user().private_identities()[index];

                match self.sub_tab {
                    SubTab::Message => {
                        if boxes.selected_contact_index().is_none() {
                            return Command::none();
                        }
                        let to_index = boxes.selected_contact_index().unwrap();
                        let to_address = &boxes.user().contacts()[to_index].address().clone();
                        let to_identity = boxes.user().private_identity_by_address(to_address);
                        if to_identity.is_none() {
                            return Command::none();
                        }
                        let to_identity = to_identity.unwrap();
                        if !to_identity.chan() {
                            // XXX restricted to chan
                            logger.error("Currently, restricted to chan");
                            return Command::none();
                        }

                        send_msg(
                            command_sender,
                            from_identity,
                            &to_identity.into(),
                            &self.message_sub_tab.subject_value,
                            &self.message_sub_tab.body_value,
                            logger,
                        );

                        boxes.set_selected_identity_index(None);
                        boxes.set_selected_contact_index(None);
                        self.message_sub_tab.subject_value = String::new();
                        self.message_sub_tab.body_value = String::new();
                    }

                    SubTab::Broadcast => {
                        send_broadcast(
                            command_sender,
                            from_identity,
                            &self.broadcast_sub_tab.subject_value,
                            &self.broadcast_sub_tab.body_value,
                            logger,
                        );

                        boxes.set_selected_identity_index(None);
                        boxes.set_selected_contact_index(None);
                        self.broadcast_sub_tab.subject_value = String::new();
                        self.broadcast_sub_tab.body_value = String::new();
                    }
                }
                Command::none()
            }
        }
    }

    pub(crate) fn view(
        &mut self,
        config: &GuiConfig,
        state: gui::State,
        boxes: &Option<Boxes>,
    ) -> Element<gui::Message> {
        let text_size = config.text_size();

        if boxes.is_none() {
            return Column::new()
                .push(Text::new("inbox/outbox database error").size(text_size))
                .into();
        }
        let boxes = boxes.as_ref().unwrap();

        let sub_tabs = self.sub_tabs.view(config, self.sub_tab);
        let mut column = Column::new().spacing(text_size / 4).push(sub_tabs);

        let color = Color {
            r: 0.0,
            g: 0.0,
            b: 1.0,
            a: 1.0,
        };

        match self.sub_tab {
            SubTab::Message => {
                let mut from = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("From: ").size(text_size).color(color));
                if let Some(index) = boxes.selected_identity_index() {
                    let identity = &boxes.user().private_identities()[index];
                    let alias = boxes.user().rich_alias(&identity.to_string());
                    from = from.push(Text::new(alias).size(text_size));
                } else {
                    from = from.push(Text::new("-- Select from Identities tab --").size(text_size));
                }

                let mut to = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("To: ").size(text_size).color(color));
                if let Some(index) = boxes.selected_contact_index() {
                    let contact = &boxes.user().contacts()[index];
                    let alias = boxes.user().rich_alias(&contact.address().to_string());
                    to = to.push(Text::new(alias).size(text_size));
                } else {
                    to = to.push(Text::new("-- Select from Contacts tab --").size(text_size));
                }

                let subject = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("Subject: ").size(text_size).color(color))
                    .push(
                        TextInput::new(
                            &mut self.message_sub_tab.subject,
                            "Subject",
                            &self.message_sub_tab.subject_value,
                            |a| gui::Message::SendMessage(Message::MessageSubjectChanged(a)),
                        )
                        .size(text_size)
                        .padding(text_size / 4),
                    );

                let body = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("Body: ").size(text_size).color(color))
                    .push(
                        TextInput::new(
                            &mut self.message_sub_tab.body,
                            "Body",
                            &self.message_sub_tab.body_value,
                            |a| gui::Message::SendMessage(Message::MessageBodyChanged(a)),
                        )
                        .size(text_size)
                        .padding(text_size / 4),
                    );

                let paste = Button::new(
                    &mut self.message_sub_tab.paste_button,
                    Text::new("Paste").size(text_size),
                )
                .on_press(gui::Message::SendMessage(Message::MessagePastePressed));

                let content = Scrollable::new(&mut self.message_sub_tab.body_scroll)
                    .max_height(256)
                    .push(
                        Text::new(&self.message_sub_tab.body_value)
                            .size(text_size)
                            .width(Length::Fill),
                    );

                column = column
                    .push(from)
                    .push(to)
                    .push(subject)
                    .push(body)
                    .push(paste)
                    .push(content);
            }

            SubTab::Broadcast => {
                let mut from = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("From: ").color(color).size(text_size));
                if let Some(index) = boxes.selected_identity_index() {
                    let identity = &boxes.user().private_identities()[index];
                    let alias = boxes.user().rich_alias(&identity.to_string());
                    from = from.push(Text::new(alias).size(text_size));
                } else {
                    from = from.push(Text::new("-- Select from Identities tab --").size(text_size));
                }

                let subject = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("Subject: ").color(color).size(text_size))
                    .push(
                        TextInput::new(
                            &mut self.broadcast_sub_tab.subject,
                            "Subject",
                            &self.broadcast_sub_tab.subject_value,
                            |a| gui::Message::SendMessage(Message::BroadcastSubjectChanged(a)),
                        )
                        .size(text_size)
                        .padding(text_size / 4),
                    );

                let body = Row::new()
                    .spacing(text_size / 2)
                    .push(Text::new("Body: ").color(color).size(text_size))
                    .push(
                        TextInput::new(
                            &mut self.broadcast_sub_tab.body,
                            "Body",
                            &self.broadcast_sub_tab.body_value,
                            |a| gui::Message::SendMessage(Message::BroadcastBodyChanged(a)),
                        )
                        .size(text_size)
                        .padding(text_size / 4),
                    );

                let paste = Button::new(
                    &mut self.broadcast_sub_tab.paste_button,
                    Text::new("Paste").size(text_size),
                )
                .on_press(gui::Message::SendMessage(Message::BroadcastPastePressed));

                let content = Scrollable::new(&mut self.broadcast_sub_tab.body_scroll)
                    .max_height(256)
                    .push(
                        Text::new(&self.broadcast_sub_tab.body_value)
                            .size(text_size)
                            .width(Length::Fill),
                    );

                column = column
                    .push(from)
                    .push(subject)
                    .push(body)
                    .push(paste)
                    .push(content);
            }
        }

        let clear_button = Button::new(&mut self.clear_button, Text::new("Clear").size(text_size))
            .on_press(gui::Message::SendMessage(Message::ClearPressed));

        let send_button = {
            let color = match state {
                gui::State::Running => style::SendButton::Enabled,
                _ => style::SendButton::Disabled,
            };
            Button::new(&mut self.send_button, Text::new("Send").size(text_size))
                .padding(text_size / 2)
                .style(color)
                .on_press(gui::Message::SendMessage(Message::SendPressed))
        };

        let buttons = Row::new()
            .spacing(text_size / 4)
            .push(clear_button)
            .push(send_button);

        column.push(buttons).into()
    }
}