diff --git a/src/gui/tui.rs b/src/gui/tui.rs index c021e57..5136681 100644 --- a/src/gui/tui.rs +++ b/src/gui/tui.rs @@ -1,23 +1,13 @@ -use std::{ - io::{Stdout, Write, stdout}, - sync::Arc, - time::Duration, -}; +use std::{io::Stdout, sync::Arc, time::Duration}; use crate::{ - ACTIVE_TASKS, APP_STATE, SHUTDOWN, + APP_STATE, SHUTDOWN, gui::{settings_panel, widgets::betterblock::draw_block_joins}, util::config_util::CONFIG, }; -use crossterm::{ - cursor::Show, - execute, - terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode}, -}; use once_cell::sync::Lazy; use ratatui::{ Frame, Terminal, - crossterm::terminal::disable_raw_mode, layout::{Constraint, Direction, Layout, Rect}, prelude::CrosstermBackend, style::Color, diff --git a/src/langu/language_manager.rs b/src/langu/language_manager.rs index 8400ac0..c903886 100644 --- a/src/langu/language_manager.rs +++ b/src/langu/language_manager.rs @@ -9,20 +9,20 @@ pub struct LanguagePack { language: HashMap, } -// Language packs need to have formatting -// Variables need to be provided - pub static LANGUAGE_PACK: Lazy> = Lazy::new(|| Mutex::new(LanguagePack::new("en_INT"))); +#[allow(dead_code)] pub fn get_language() -> LanguagePack { LANGUAGE_PACK.lock().unwrap().clone() } +#[allow(dead_code)] pub fn get_languages() -> Vec { file_util::get_children("languages") } +#[allow(dead_code)] pub fn set_language(language: &str) { LANGUAGE_PACK.lock().unwrap().language.clear(); LANGUAGE_PACK.lock().unwrap().load_language(language); diff --git a/src/main.rs b/src/main.rs index 444622a..027755b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -146,7 +146,7 @@ async fn main() { break; } let omikron: Arc = Arc::new(OmikronConnection::new()); - omikron.connect(sb.clone()).await; + omikron.connect().await; let mut omikron_connection = OMIKRON_CONNECTION.write().await; *omikron_connection = Some(omikron.clone()); log_message_trans("setup_completed"); diff --git a/src/omikron/omikron_connection.rs b/src/omikron/omikron_connection.rs index 40656b4..e0169d4 100644 --- a/src/omikron/omikron_connection.rs +++ b/src/omikron/omikron_connection.rs @@ -14,15 +14,14 @@ use crate::{ util::{config_util::CONFIG, crypto_helper}, }; use dashmap::DashMap; +use futures::Stream; use futures::stream::{SplitSink, SplitStream}; -use futures::{FutureExt, Stream}; use futures_util::sink::Sink; use futures_util::{SinkExt, StreamExt}; use hyper::upgrade::Upgraded; use hyper_util::rt::TokioIo; use json::JsonValue; use json::number::Number; -use pkcs8::DecodePrivateKey; use std::collections::HashMap; use std::sync::{Arc, LazyLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -31,7 +30,6 @@ use tokio::time::{Duration, Instant, sleep}; use tokio_tungstenite::{connect_async, tungstenite::protocol::Message}; use tungstenite::Utf8Bytes; use uuid::Uuid; -use warp::reply::Json; pub static OMIKRON_CONNECTION: LazyLock>>>> = LazyLock::new(|| Arc::new(RwLock::new(None))); @@ -50,7 +48,6 @@ pub struct OmikronConnection { pub(crate) writer: Arc + Send + Unpin>>>>, waiting: Arc>>, - pingpong: Arc>>>, pub last_ping: Arc>, pub message_send_times: Arc>>, pub is_connected: Arc>, @@ -63,7 +60,6 @@ impl OmikronConnection { user_id: Arc::new(RwLock::new(0)), writer: Arc::new(Mutex::new(None)), waiting: Arc::new(DashMap::new()), - pingpong: Arc::new(Mutex::new(None)), last_ping: Arc::new(Mutex::new(-1)), message_send_times: Arc::new(Mutex::new(HashMap::new())), is_connected: Arc::new(Mutex::new(false)), @@ -79,7 +75,6 @@ impl OmikronConnection { writer: Arc::new(Mutex::new(Some(Box::new(writer) as Box + Send + Unpin>))), waiting: Arc::new(DashMap::new()), - pingpong: Arc::new(Mutex::new(None)), last_ping: Arc::new(Mutex::new(-1)), message_send_times: Arc::new(Mutex::new(HashMap::new())), is_connected: Arc::new(Mutex::new(true)), @@ -94,8 +89,8 @@ impl OmikronConnection { pub async fn is_connected(&self) -> bool { *self.is_connected.lock().await } - /// Connect loop with retry - pub async fn connect(self: &Arc, user_ids: String) { + + pub async fn connect(self: &Arc) { if self.is_connected().await { return; } @@ -104,10 +99,9 @@ impl OmikronConnection { let iota_id = conf.get_iota_id(); let public_key = conf.get_public_key(); let private_key = conf.get_private_key(); - drop(conf); // release read lock + drop(conf); if iota_id == 0 || public_key.is_none() || private_key.is_none() { - // Registration flow log_message_trans("iota_register_new"); let key_pair = crypto_helper::generate_keypair(); let public_key_base64 = crypto_helper::public_key_to_base64(&key_pair.public); @@ -120,7 +114,6 @@ impl OmikronConnection { drop(conf_write); if self.connect_internal().await { - // a new helper function to just connect match self .clone() .await_response( @@ -148,7 +141,6 @@ impl OmikronConnection { } } } else { - // Login flow if self.connect_internal().await { self.send_message( &CommunicationValue::new(CommunicationType::identification).add_data( @@ -167,7 +159,6 @@ impl OmikronConnection { } log_message_trans("omikron_connecting"); - // connect to omikron let conf = CONFIG.read().await; let addr = conf .get("omikron_addr") @@ -223,7 +214,6 @@ impl OmikronConnection { *self.user_id.write().await = user_id; } - /// Listener for all incoming messages async fn spawn_listener( self: &Arc, mut read_half: Box> + Send + Unpin>, @@ -444,7 +434,7 @@ impl OmikronConnection { let sender_id = &cv.get_sender(); let receiver_id = &cv.get_receiver(); - chat_files::change_message_state( + let _ = chat_files::change_message_state( cv.get_data(DataTypes::send_time) .unwrap_or(&JsonValue::new_object()) .as_i64() @@ -520,7 +510,8 @@ impl OmikronConnection { DataTypes::message_state, JsonValue::from(ms.as_str()), ), - ); + ) + .await; } else { self.send_message( &CommunicationValue::new(CommunicationType::message_state) @@ -535,7 +526,8 @@ impl OmikronConnection { DataTypes::message_state, JsonValue::from(MessageState::Send.as_str()), ), - ); + ) + .await; } return; } diff --git a/src/terms/doc.rs b/src/terms/doc.rs new file mode 100644 index 0000000..f46872c --- /dev/null +++ b/src/terms/doc.rs @@ -0,0 +1,53 @@ +use json::{JsonValue, object::Object}; + +use crate::{terms::terms_getter::Type, util::file_util::load_file}; + +#[derive(Clone)] +#[allow(unused)] +pub struct Doc { + version: String, + hash: String, + doc_type: Type, + timestamp: u64, +} + +#[allow(dead_code)] +impl Doc { + pub fn get_version(&self) -> String { + self.version.clone() + } + pub fn get_hash(&self) -> String { + self.hash.clone() + } + pub fn get_time(&self) -> u64 { + self.timestamp.clone() + } + pub fn get_content(&self) -> String { + load_file( + format!("docs/{}/", self.doc_type.to_str()).as_str(), + format!("{}.md", self.version).as_str(), + ) + } + + pub fn to_json(&self) -> JsonValue { + let mut json = JsonValue::new_object(); + + let _ = json.insert("version", self.version.clone()); + let _ = json.insert("hash", self.hash.clone()); + let _ = json.insert("unix", self.timestamp.clone()); + + json + } + pub fn from_json(doc_type: Type, json: Object) -> Option { + let hash = json.get("hash")?.as_str()?.to_string(); + let version = json.get("version")?.as_str()?.to_string(); + let timestamp = json.get("unix")?.as_u64()?; + + Some(Doc { + version, + hash, + doc_type, + timestamp, + }) + } +} diff --git a/src/terms/md_viewer.rs b/src/terms/md_viewer.rs index dde8885..6a80ac5 100644 --- a/src/terms/md_viewer.rs +++ b/src/terms/md_viewer.rs @@ -306,6 +306,14 @@ fn table_to_lines(table: Vec>) -> Vec { } let header = &table[0]; + let mut column_heights = vec![0; table[0].len()]; + + for row in table.iter().skip(1) { + for (i, cell) in row.iter().enumerate() { + let lines = cell.lines().count().max(1); + column_heights[i] += lines; + } + } let widths: Vec = header .iter() @@ -383,22 +391,47 @@ fn wrap_cell(cell: &str, width: usize) -> Vec { return vec![String::new()]; } - let mut out = Vec::new(); - let mut chars = cell.chars(); + let mut lines = Vec::new(); + let mut current = String::new(); - loop { - let line: String = chars.by_ref().take(width).collect(); - if line.is_empty() { - break; + for word in cell.split_whitespace() { + let word_len = word.chars().count(); + let current_len = current.chars().count(); + + if current_len == 0 { + if word_len <= width { + current.push_str(word); + } else { + for chunk in word.chars().collect::>().chunks(width) { + lines.push(chunk.iter().collect()); + } + } + } else if current_len + 1 + word_len <= width { + current.push(' '); + current.push_str(word); + } else { + lines.push(current); + current = String::new(); + + if word_len <= width { + current.push_str(word); + } else { + for chunk in word.chars().collect::>().chunks(width) { + lines.push(chunk.iter().collect()); + } + } } - out.push(line); } - if out.is_empty() { - out.push(String::new()); + if !current.is_empty() { + lines.push(current); } - out + if lines.is_empty() { + lines.push(String::new()); + } + + lines } fn flush_span(spans: &mut Vec, buf: &mut String, style: Style) { diff --git a/src/terms/mod.rs b/src/terms/mod.rs index 7cf232b..4183633 100644 --- a/src/terms/mod.rs +++ b/src/terms/mod.rs @@ -1,3 +1,4 @@ +pub mod doc; pub mod md_viewer; pub mod terms_checker; pub mod terms_getter; diff --git a/src/terms/terms_checker.rs b/src/terms/terms_checker.rs index 368eab3..e34023e 100644 --- a/src/terms/terms_checker.rs +++ b/src/terms/terms_checker.rs @@ -1,7 +1,7 @@ use crate::{ terms::{ md_viewer::FileViewer, - terms_getter::{Type, get_link, get_terms}, + terms_getter::{Type, get_current_docs, get_link, get_terms}, }, util::file_util::{load_file, save_file}, }; @@ -41,7 +41,9 @@ impl ConsentManager { }, }; let state = state.sanitize(); - save_file("", "agreements", &state.to_string()); + if let Ok(string) = state.to_string().await { + save_file("", "agreements", &string); + } state }; @@ -92,24 +94,46 @@ impl ConsentUiState { self } - fn to_string(self) -> String { + async fn to_string(self) -> Result { let current_secs = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); - format!( - "\"EULA=true\" indicates that you read and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\ + + if let Some((eula, tos, pp)) = get_current_docs().await { + Ok(format!( + "\ + \"EULA=true\" indicates that you read, understood and accepted the End User Licence agreement. You can find our EULA at https://legal.tensamin.net/eula/\ \nEULA={}\ - \n\"PrivacyPolicy=true\" indicates that you read and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\ + \nEULA-VERSION={}\ + \nEULA-HASH={}\ + \n\"PrivacyPolicy=true\" indicates that you read, understood and accepted the Privacy Policy. You can find our Privacy Policy at https://legal.tensamin.net/privacy-policy/\ \nPrivacyPolicy={}\ - \n\"ToS=true\" indicates that you read and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\ + \nPrivacyPolicy-VERSION={}\ + \nPrivacyPolicy-HASH={}\ + \n\"ToS=true\" indicates that you read, understood and accepted the Terms of Service. You can find our Terms of Service at https://legal.tensamin.net/terms-of-service/\ \nToS={}\ + \nToS-VERSION={}\ + \nToS-HASH={}\ \nThis file reflects the current consent state used by the application.\ \nIt may be regenerated or overwritten by the application.\ \nThis file was last edited by Tensamin at:\ - \nUNIX-SECOND={}", - self.eula, self.pp, self.tos, current_secs - ) + \nUNIX-SECOND={}\ + ", + self.eula, + eula.get_version(), + eula.get_hash(), + self.tos, + tos.get_version(), + tos.get_hash(), + self.pp, + pp.get_version(), + pp.get_hash(), + current_secs + )) + } else { + Err(()) + } } fn from_str(s: &str) -> Self { @@ -195,7 +219,7 @@ async fn run_consent_ui() -> UserChoice { return; } - let max_width = 132; + let max_width = 150; let max_height = 20; let content_width = if max_width < size.width { max_width } else { size.width} ; @@ -230,30 +254,43 @@ async fn run_consent_ui() -> UserChoice { }; let (mut optional_lines, agree_lines): (Vec, Vec<&str>) = - if size.width > 132 { + if size.width > 143 { ( vec![7, 3, 7, 4], vec![ "", - "By selecting Continue, you confirm that you have read and agree to the End User License Agreement and applicable Terms of Service.", + "By selecting Continue, you confirm that you have read, understood and agree to the End User License Agreement and applicable Terms of Service.", "", "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", "", "While having a document selected press O to view in this UI or press L to open as a link.", ] ) - } else if size.width > 68 { + } else if size.width > 92 { ( vec![8, 3, 8, 4], vec![ "", - "By selecting Continue, you confirm that you have read and agree", - "to the End User License Agreement and applicable Terms of Service.", + "By selecting Continue, you confirm that you have read, understood and agree to the End User", + "License Agreement and applicable Terms of Service.", + "", + "Tensamin services require acceptance of the Terms of Service and Privacy Policy.", + "", + "While having a document selected press O to view in this UI or press L to open as a link.", + ] + ) + } else if size.width > 73 { + ( + vec![8, 3, 8, 4], + vec![ + "", + "By selecting Continue, you confirm that you have read, understood and", + "agree to the End User License Agreement and applicable Terms of Service.", "", "Tensamin services require acceptance of the ToS and Privacy Policy.", "", - "While having a document selected press O to view in this UI or", - "press L to open as a link.", + "While having a document selected press O to view in this UI or press L", + "to open as a link.", ] ) } else { @@ -262,8 +299,8 @@ async fn run_consent_ui() -> UserChoice { vec![ "", "By selecting Continue, you confirm that you have", - "read and agree to the End User License Agreement", - "and applicable Terms of Service.", + "read, understood and agree to the End User License", + "Agreement and applicable Terms of Service.", "", "Tensamin services require acceptance of the", "Terms of Service and Privacy Policy.", @@ -340,7 +377,7 @@ async fn run_consent_ui() -> UserChoice { } let consent_block = Paragraph::new(Text::from(text_lines)) - .block(Block::default().title(" Tensamin User Consent ").borders(Borders::ALL)); + .block(Block::default().title(" Tensamin User Consent [Q to Quit] ").borders(Borders::ALL)); f.render_widget(consent_block, chunks[0]); draw_buttons(f, chunks[1], &state); @@ -358,8 +395,8 @@ async fn run_consent_ui() -> UserChoice { } match key.code { KeyCode::Esc => break UserChoice::Deny, - KeyCode::Up => state.prev(), - KeyCode::Down | KeyCode::Tab => state.next(), + KeyCode::Up | KeyCode::Left => state.prev(), + KeyCode::Down | KeyCode::Right | KeyCode::Tab => state.next(), KeyCode::Char('q') | KeyCode::Char('Q') => break UserChoice::Deny, KeyCode::Char('o') | KeyCode::Char('O') => { let terms_type = match state.focus { @@ -516,7 +553,7 @@ fn draw_buttons(f: &mut ratatui::Frame, area: Rect, state: &ConsentUiState) { .fg(Color::Black) .bg(Color::Green) .add_modifier(Modifier::BOLD) - } else if state.can_continue() && state.pp { + } else if state.can_continue_all() { Style::default().fg(Color::Green) } else { Style::default().fg(Color::DarkGray) diff --git a/src/terms/terms_getter.rs b/src/terms/terms_getter.rs index 5f8958c..4de8298 100644 --- a/src/terms/terms_getter.rs +++ b/src/terms/terms_getter.rs @@ -1,3 +1,7 @@ +use json::JsonValue::Object; + +use crate::terms::doc::Doc; + #[derive(Clone)] pub enum Type { EULA, @@ -10,7 +14,7 @@ impl Type { match self { Self::EULA => "eula", Self::TOS => "tos", - Self::PP => "pp", + Self::PP => "privacy", } } pub fn to_string(&self) -> String { @@ -21,9 +25,40 @@ impl Type { } } } + pub fn get_link(terms_type: Type) -> String { format!("https://legal.tensamin.net/{}/", terms_type.to_str()) } + +pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> { + let body = reqwest::get("https://legal.tensamin.net/api/current/") + .await + .ok()? + .text() + .await + .ok()?; + + let json = json::parse(&body).ok()?; + + if let Object(eula) = &json["eula"] { + if let Object(tos) = &json["tos"] { + if let Object(pp) = &json["pp"] { + Some(( + Doc::from_json(Type::EULA, eula.clone())?, + Doc::from_json(Type::TOS, tos.clone())?, + Doc::from_json(Type::PP, pp.clone())?, + )) + } else { + None + } + } else { + None + } + } else { + None + } +} + pub async fn get_terms(terms_type: Type) -> Option { let body = reqwest::get(format!( "https://legal.tensamin.net/api/text/{}/", diff --git a/src/users/user_manager.rs b/src/users/user_manager.rs index 7155a4c..8242dee 100644 --- a/src/users/user_manager.rs +++ b/src/users/user_manager.rs @@ -22,7 +22,7 @@ use x448::{PublicKey, Secret}; static USERS: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); static UNIQUE: Lazy> = Lazy::new(|| Mutex::new(false)); -// uuid::private_key +#[allow(dead_code)] pub async fn load_from_tu(username: &str) -> Result<(), ()> { let file_content = load_file("", &format!("{}.tu", username)); let segments = file_content.split("::").collect::>(); @@ -181,6 +181,7 @@ pub async fn load_users() -> io::Result<()> { Ok(()) } +#[allow(dead_code)] pub fn set_unique(val: bool) { *UNIQUE.lock().unwrap() = val; } diff --git a/src/users/user_profile.rs b/src/users/user_profile.rs index aef1d85..5d60277 100644 --- a/src/users/user_profile.rs +++ b/src/users/user_profile.rs @@ -1,7 +1,5 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use crate::gui::log_panel::log_message; -use crate::users::user_manager; use crate::util::file_util::{has_file, load_file, used_dir_space}; use base64::{Engine as _, engine::general_purpose}; use json::{JsonValue, object}; @@ -84,7 +82,7 @@ impl UserProfile { let created_at = j["created_at"].as_i64()?; let display_name = j["display_name"].as_str().map(|s| s.to_string()); - let mut up = UserProfile { + let up = UserProfile { user_id, username, display_name, @@ -110,6 +108,7 @@ impl UserProfile { Some(up) } + #[allow(dead_code)] pub fn randomize_reset_token(&mut self) -> String { let mut bytes = [0u8; 192]; OsRng.fill(bytes.as_mut()); @@ -118,6 +117,7 @@ impl UserProfile { new_token } + #[allow(dead_code)] pub fn get_display_name(&self) -> String { self.display_name .clone() diff --git a/src/util/crypto_helper.rs b/src/util/crypto_helper.rs index 9d9a078..4a7fd76 100644 --- a/src/util/crypto_helper.rs +++ b/src/util/crypto_helper.rs @@ -9,6 +9,7 @@ use x448::{PublicKey, Secret, SharedSecret}; /// Errors for crypto opertions #[derive(Debug)] +#[allow(dead_code)] pub enum CryptoError { Base64Decode(base64::DecodeError), InvalidKey, @@ -55,6 +56,7 @@ pub fn load_secret_key(base64_secret: &str) -> Option { Secret::from_bytes(&bytes) } +#[allow(dead_code)] fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(shared.as_bytes()); @@ -64,6 +66,7 @@ fn derive_aes_key(shared: &SharedSecret) -> [u8; 32] { key } +#[allow(dead_code)] pub fn encrypt( base64_secret: &str, base64_peer_pub: &str, @@ -89,6 +92,7 @@ pub fn encrypt( Ok(STANDARD.encode(&out)) } +#[allow(dead_code)] pub fn decrypt( base64_secret: &str, base64_peer_pub: &str, diff --git a/src/util/crypto_util.rs b/src/util/crypto_util.rs index df010e0..db427e4 100644 --- a/src/util/crypto_util.rs +++ b/src/util/crypto_util.rs @@ -9,6 +9,7 @@ use x448::{PublicKey, Secret}; // --- Custom Errors --- #[derive(Debug)] +#[allow(dead_code)] pub enum SecurePayloadError { InvalidBase64, InvalidHex, @@ -19,6 +20,7 @@ pub enum SecurePayloadError { // --- Data Format Enum --- #[derive(Clone, Copy, Debug)] +#[allow(dead_code)] pub enum DataFormat { Raw, Base64, @@ -42,6 +44,7 @@ impl Clone for SecurePayload { } } +#[allow(dead_code)] impl SecurePayload { /// Clear Constructor: Takes data in any format and the user's private key. pub fn new>( diff --git a/src/util/file_util.rs b/src/util/file_util.rs index ab00781..c27f9f9 100644 --- a/src/util/file_util.rs +++ b/src/util/file_util.rs @@ -18,11 +18,13 @@ pub fn delete_file(path: &str, name: &str) -> bool { fs::remove_file(file).is_ok() } +#[allow(dead_code)] pub fn delete_directory(path: &str) -> bool { let dir = Path::new(&get_directory()).join(path); delete_dir_recursive(&dir) } +#[allow(dead_code)] fn delete_dir_recursive(directory: &Path) -> bool { if !directory.exists() { return false; @@ -38,6 +40,7 @@ fn delete_dir_recursive(directory: &Path) -> bool { true } +#[allow(dead_code)] pub fn delete_user_directory(user_id: Uuid) { let user_dir = Path::new(&get_directory()) .join("users") @@ -187,6 +190,7 @@ pub fn get_directory() -> String { .to_string() } +#[allow(dead_code)] pub fn used_space() -> u64 { get_directory_size(&PathBuf::from(get_directory())) } @@ -208,6 +212,7 @@ pub fn get_directory_size(directory: &Path) -> u64 { size } +#[allow(dead_code)] pub fn get_designed_storage(user_id: Uuid) -> String { let user_dir = Path::new(&get_directory()) .join("users") @@ -215,6 +220,7 @@ pub fn get_designed_storage(user_id: Uuid) -> String { design_byte(get_directory_size(&user_dir)) } +#[allow(dead_code)] pub fn design_byte(bytes: u64) -> String { let mut hr_size = format!("{:.2}B", bytes as f64); let k = bytes as f64 / 1024.0; @@ -234,6 +240,7 @@ pub fn design_byte(bytes: u64) -> String { hr_size } +#[allow(dead_code)] pub fn get_used_ram() -> String { let mut sys = System::new_all(); sys.refresh_all(); @@ -243,6 +250,7 @@ pub fn get_used_ram() -> String { } // Helper to download the zip file content to a file on disk +#[allow(dead_code)] async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box> { let response = reqwest::get(url).await?; @@ -258,6 +266,7 @@ async fn download_zip(url: &str, as_name: &Path) -> Result<(), Box