use std::time::Duration; use client_shared::{API_CONNECTION_TIMEOUT, message::Message, persistence::Appdata, utils::fix_url}; use cursive::{views::TextArea, Cursive}; use utils::{binary::checksum, hash::Hashable as _, serialize::Serialize as _}; use crate::{get_appdata, ui}; // TODO (low): Create proper user objects. Related: add a way to verify a user. pub fn on_user_click(siv: &mut Cursive, user: &str) { let language = get_appdata(siv).persistent_data.language; ui::alert(siv, ui::Labels::User.localize(language), user.into()); } pub fn on_message_click(siv: &mut Cursive, message_id: &str) { let appdata = get_appdata(siv); let language = appdata.persistent_data.language; let message = appdata.messages.get(message_id); if let Some(message) = message { ui::copyable( siv, ui::Labels::Message.localize(appdata.persistent_data.language), format!( "{}: {}\n{}: {}\n{}: {}\n{}: {}", ui::Labels::Time.localize(language), message.time, ui::Labels::Channel.localize(language), message.channel, ui::Labels::Sender.localize(language), message.sender, ui::Labels::Content.localize(language), message.content ), ); } } // TODO: Sending in progress pop-up for slow connections. Currently just hangs the application. pub fn on_input_submit(siv: &mut Cursive, input_field_id: &str) { let text = siv .call_on_name(input_field_id, |view: &mut TextArea| { view.get_content().to_string() }) .expect("Failed to retrieve input field content."); let appdata = get_appdata(siv); let language = appdata.persistent_data.language; let channel = appdata.persistent_data.current_channel; let message = Message::new(&appdata.persistent_data.username, &text, &channel); if message.is_valid() { // NOTE: // If an error was shown during send, it's likely that loading will fail too // so we don't need to show it again let mut error_dialogue_shown = false; match send_message(siv, message) { Ok(_) => clear_input_field(siv), Err(e) => { error_dialogue_shown = true; ui::error(siv, ui::Labels::FailedToSendMessage(e).localize(language)) } }; let res = load_messages(siv); if let Err(e) = res { if !error_dialogue_shown { ui::error(siv, ui::Labels::FailedToLoadMessages(e).localize(language)); } } else { ui::alert( siv, ui::Labels::Success.localize(language), ui::Labels::SuccessfullySentMessage.localize(language), ); } } else if !text.is_empty() { // Invalid message ui::error(siv, ui::Labels::InvalidMessage.localize(language)); } ui::visual_update(siv); } pub fn clear_input_field(siv: &mut Cursive) { siv.call_on_name(ui::INPUT_FIELD_ID, |view: &mut TextArea| { view.set_content(""); }) .expect("Failed to clear input field."); } type StatusCode = u16; #[allow(dead_code)] pub enum NetworkError { ReqwestError(reqwest::Error), StatusCodeError(StatusCode), EncryptionError(utils::aes::AesError), UrlError(client_shared::utils::UrlError), } pub fn send_message(siv: &mut Cursive, mut message: Message) -> Result<(), NetworkError> { message.sanitize(); let appdata = get_appdata(siv); let url = fix_url(&appdata.persistent_data.api_endpoint).map_err(|x| NetworkError::UrlError(x))?; let password = appdata.persistent_data.api_password; let mut bytes = message.serialize_checked(); utils::aes::encrypt_cbc(&mut bytes, &password); bytes.push(checksum(&bytes)); let str = utils::binary::bin2hex(bytes); let resp = reqwest::blocking::Client::new() .post(format!("{}", url)) .body(str) .timeout(Duration::from_secs(API_CONNECTION_TIMEOUT)) .send() .map_err(|e| NetworkError::ReqwestError(e))?; if resp.status().is_success() { Ok(()) } else { Err(NetworkError::StatusCodeError(resp.status().as_u16())) } } pub fn load_messages(siv: &mut Cursive) -> Result<(), NetworkError> { let appdata = get_appdata(siv); let url = fix_url(&appdata.persistent_data.api_endpoint).map_err(|x| NetworkError::UrlError(x))?; let password = appdata.persistent_data.api_password; let resp = reqwest::blocking::Client::new() .get(url) .timeout(Duration::from_secs(API_CONNECTION_TIMEOUT)) .send() .map_err(|e| NetworkError::ReqwestError(e))?; if resp.status() != reqwest::StatusCode::OK { return Err(NetworkError::StatusCodeError(resp.status().as_u16())); } let bytes = resp.bytes().map_err(|e| NetworkError::ReqwestError(e))?; let contents = String::from_utf8_lossy(&bytes); let mut messages = vec![]; for message_ser_hex in contents.split(",") { let Some(mut message_ser_bin) = utils::binary::hex2bin(message_ser_hex) else { continue; }; if message_ser_bin.len() < 2 { continue; } // Checksum before decryption if checksum(&message_ser_bin[..message_ser_bin.len()-1]) != message_ser_bin[message_ser_bin.len()-1] { continue; } // Remove checksum message_ser_bin.pop(); if utils::aes::decrypt_cbc(&mut message_ser_bin, &password).is_err() { continue; } let Ok(mut message) = Message::deserialize_checked(message_ser_bin) else { continue; }; if message.is_valid() { message.sanitize(); messages.push(message); } } siv.with_user_data(|appdata: &mut Appdata| { for message in messages { appdata.messages.insert(message.hash().to_string(), message); } }); Ok(()) }