Explorar o código

Fix logging and reorganize code

Rain hai 1 mes
pai
achega
689ac8a0d1

+ 1 - 0
.gitignore

@@ -1,3 +1,4 @@
 target/
 Cargo.lock
 savedata.bin
+*-log.txt

+ 1 - 0
Cargo.toml

@@ -2,6 +2,7 @@
 resolver = "2"
 members = [
     "client",
+    "client_shared",
     "logging",
     "procmacro",
     "server",

+ 1 - 1
client/Cargo.toml

@@ -5,12 +5,12 @@ edition = "2021"
 default-run = "client"
 
 [dependencies]
+client_shared = { path = "../client_shared" }
 utils = { path = "../utils" }
 logging = { path = "../logging" }
 
 cursive = "0.21"
 cli-clipboard = "0.4.0"
-url = "2.5.8"
 
 # TODO: Write an HTTP client.
 reqwest = { version = "0.12.15", features = [ "blocking" ] }

+ 7 - 28
client/src/actions.rs

@@ -1,10 +1,11 @@
-use std::{str::FromStr, time::Duration};
+use std::time::Duration;
 
+use client_shared::{API_CONNECTION_TIMEOUT, message::Message, persistence::Appdata, utils::fix_url};
 use cursive::{views::TextArea, Cursive};
-use url::Url;
 use utils::{binary::checksum, hash::Hashable as _, serialize::Serialize as _};
 
-use crate::{API_CONNECTION_TIMEOUT, API_DEFAULT_PORT, Appdata, get_appdata, message::Message, ui};
+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) {
@@ -92,35 +93,13 @@ type StatusCode = u16;
 pub enum NetworkError {
     ReqwestError(reqwest::Error),
     StatusCodeError(StatusCode),
-    InvalidUrl,
     EncryptionError(utils::aes::AesError),
-}
-
-fn fix_url(url: &str) -> Result<String, NetworkError> {
-    let mut url: String = url.to_string();
-
-    if !url.starts_with("http://") && !url.starts_with("https://") {
-        url = format!("http://{}", url);
-    }
-
-    let mut url = Url::from_str(&url).map_err(|_| NetworkError::InvalidUrl)?;
-
-    if url.port().is_none() {
-        url.set_port(Some(API_DEFAULT_PORT)).map_err(|_| NetworkError::InvalidUrl)?;
-    }
-
-    let mut url = url.to_string();
-
-    if !url.ends_with('/') {
-        url.push('/');
-    }
-
-    Ok(url)
+    UrlError(client_shared::utils::UrlError),
 }
 
 pub fn send_message(siv: &mut Cursive, message: Message) -> Result<(), NetworkError> {
     let appdata = get_appdata(siv);
-    let url = fix_url(&appdata.persistent_data.api_endpoint)?;
+    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();
@@ -145,7 +124,7 @@ pub fn send_message(siv: &mut Cursive, message: Message) -> Result<(), NetworkEr
 
 pub fn load_messages(siv: &mut Cursive) -> Result<(), NetworkError> {
     let appdata = get_appdata(siv);
-    let url = fix_url(&appdata.persistent_data.api_endpoint)?;
+    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()

+ 16 - 96
client/src/main.rs

@@ -1,126 +1,46 @@
 mod actions;
-mod message;
-mod persistence;
 mod ui;
 
+use client_shared::{LOGO, LOGO_DURATION};
 use cursive::align::Align;
-use cursive::reexports::log;
+use cursive::view::Resizable as _;
 use cursive::views::TextView;
 use cursive::{event, Cursive};
-use cursive::{traits::*, CursiveExt as _};
-use message::Message;
-use std::collections::HashMap;
-use std::io::{Read as _, Write as _};
+use cursive::CursiveExt as _;
+use std::ffi::OsString;
+use std::str::FromStr;
 use std::thread;
 use std::time::Duration;
-use utils::serialize::Serialize;
 
 use ui::INPUT_FIELD_ID;
 
-use crate::persistence::Savedata;
-
-const MAX_MESSAGE_LENGTH: usize = 512;
-const MAX_USERNAME_LENGTH: usize = 16;
-const DEFAULT_USERNAME_PREFIX: &str = "Myst";
-const DEFAULT_CHANNEL: &str = "Root";
-const DEFAULT_PASSWORD: &str = "null";
-
-const API_CONNECTION_TIMEOUT: u64 = 2;
-const API_DEFAULT_PORT: u16 = 13337;
-
-const INVALID_MESSAGE_IDENT: &str = "ERR";
-
-const LOGO: &str = include_str!("../assets/logo.txt");
-const LOGO_DURATION: u128 = 500;
-
-const SAVE_FILE: &str = "savedata.bin";
-const SAVE_FILE_FUZZY: u64 = 0b0110110001101001011001110110110101100001001000000101100001000100;
-
-#[derive(Clone)]
-pub struct Appdata {
-    pub persistent_data: Savedata,
-
-    pub messages: HashMap<String, Message>,
-    pub quick_close_window_count: usize,
-    pub local_channels: Vec<String>,
-    pub intro_screen_shown_until: u128,
-}
-
-impl Default for Appdata {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl Appdata {
-    pub fn new() -> Self {
-        Self {
-            messages: HashMap::new(),
-            quick_close_window_count: 0,
-            local_channels: vec![DEFAULT_CHANNEL.to_string()],
-            intro_screen_shown_until: 0,
-            persistent_data: Savedata::new(),
-        }
-    }
-}
+use client_shared::persistence::{Appdata, load_appdata, save_appdata};
 
 pub fn get_appdata(siv: &mut Cursive) -> Appdata {
     siv.with_user_data(|appdata: &mut Appdata| appdata.clone())
         .expect("Failed to retrieve appdata.")
 }
 
-pub fn save_appdata(siv: &mut Cursive) {
-    let appdata = get_appdata(siv);
-
-    let savedata = persistence::Savedata::from(appdata);
-    let bytes = savedata.serialize_checked();
-    let fuzzy_bytes = utils::binary::fuzzy_bytes(bytes, SAVE_FILE_FUZZY);
-
-    let mut file = std::fs::File::create(SAVE_FILE).expect("Failed to create savedata file.");
-    file.write_all(&fuzzy_bytes)
-        .expect("Failed to write savedata file.");
-
-    log::info!("Wrote savadata to file.");
-}
-
-pub fn load_appdata(siv: &mut Cursive) -> std::io::Result<()> {
-    let mut df = logging::warn_deferred("Savedata file not found; using defaults");
-
-    let mut file = std::fs::File::open(SAVE_FILE)?;
-
-    let mut bytes = Vec::new();
-    file.read_to_end(&mut bytes)?;
-    let useful_bytes = utils::binary::fuzzy_bytes(bytes, SAVE_FILE_FUZZY);
-
-    df.cancel();
-
-    let Ok(savedata) = persistence::Savedata::deserialize_checked(useful_bytes) else {
-        // If the file is corrupted, create a new one
-        logging::warn("Savedata file corrupted; using defaults.");
-        return Ok(());
-    };
-
-    let appdata = Appdata::from(savedata);
-    siv.set_user_data(appdata);
-
-    logging::info("Savedata file loaded.");
-    Ok(())
-}
-
 fn main() {
     if cfg!(debug_assertions) {
         logging::set_global_log_level(logging::LogLevel::Trace);
     } else {
-        logging::set_global_log_level(logging::LogLevel::Info);
+        logging::set_global_log_level(logging::LogLevel::Debug);
     }
 
+    logging::set_global_log_path(Some(OsString::from_str("./client-log.txt").unwrap()));
+
     utils::rng::shuffle_rng();
 
     let mut siv = Cursive::default();
-    siv.set_user_data(Appdata::new());
 
     // TODO (low): Add a notice when the file is corrupted.
-    let _ = load_appdata(&mut siv);
+    let appdata = match load_appdata() {
+        Ok(Some(x)) => x,
+        _ => Appdata::new(),
+    };
+
+    siv.set_user_data(appdata);
 
     // Global hotkeys
     siv.add_global_callback(event::Key::Backspace, |siv| {
@@ -162,7 +82,7 @@ fn main() {
                             let _ = actions::load_messages(siv);
                         }
 
-                        save_appdata(siv);
+                        save_appdata(get_appdata(siv));
                     }))
                     .expect("Failed to send callback from background thread.");
 

+ 14 - 20
client/src/ui.rs

@@ -1,4 +1,7 @@
 use cli_clipboard::ClipboardProvider as _;
+use client_shared::message::{self, MessageSanitized};
+use client_shared::{DEFAULT_PASSWORD, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH};
+use client_shared::ui::Language;
 use cursive::view::{Nameable as _, Resizable as _};
 use cursive::views::{
     Button, Dialog, DummyView, EditView, LinearLayout, NamedView, Panel, ScrollView, SelectView,
@@ -8,10 +11,8 @@ use cursive::{event, Cursive};
 use utils::hash::Hashable as _;
 
 use crate::actions::{self, NetworkError};
-use crate::message::MessageSanitized;
 use crate::{
-    get_appdata, message, save_appdata, Appdata, DEFAULT_PASSWORD, MAX_MESSAGE_LENGTH,
-    MAX_USERNAME_LENGTH,
+    get_appdata, save_appdata, Appdata
 };
 
 pub const USERS_PANEL_ID: &str = "users_view_id";
@@ -39,7 +40,7 @@ pub const INFO_BUTTON_ID: &str = "INFO_BUTTON_ID";
 
 pub const DIALOGUE_MIN_SIZE: (u16, u16) = (20, 5);
 pub const SERVER_SETTINGS_FIELD_SIZE: (usize, usize) = (60, 1);
-pub const USER_PANEL_SIZE: usize = crate::MAX_USERNAME_LENGTH + 2;
+pub const USER_PANEL_SIZE: usize = MAX_USERNAME_LENGTH + 2;
 
 #[allow(unused)]
 pub enum Labels {
@@ -83,13 +84,6 @@ pub enum Labels {
     QuitButton,
 }
 
-#[derive(Debug, Clone, Copy)]
-pub enum Language {
-    English,
-    Dutch,
-    Japanese,
-}
-
 impl Labels {
     // TODO (low): Double check the translations
     pub fn localize<'a>(&self, language: Language) -> String {
@@ -146,7 +140,7 @@ impl Labels {
                             ]
                         }
                     }
-                    NetworkError::InvalidUrl => {
+                    NetworkError::UrlError(_) => {
                         reason_buf = [
                             "Invalid URL.".to_string(),
                             "Ongeldig URL.".to_string(),
@@ -188,7 +182,7 @@ impl Labels {
                             format!("ステータスコード: {code}"),
                         ];
                     }
-                    NetworkError::InvalidUrl => {
+                    NetworkError::UrlError(_) => {
                         reason_buf = [
                             "Invalid URL.".to_string(),
                             "Ongeldig URL.".to_string(),
@@ -388,7 +382,7 @@ pub fn visual_update(siv: &mut Cursive) {
 
     // --- Messages ---
 
-    let mut messages: Vec<message::MessageSanitized> = appdata
+    let mut messages: Vec<MessageSanitized> = appdata
         .messages
         .clone()
         .values()
@@ -399,7 +393,7 @@ pub fn visual_update(siv: &mut Cursive) {
 
     // Remove blocked phrases
     for message in messages.iter_mut() {
-        message.remove_blocked_phrases(siv);
+        message.remove_blocked_phrases(&appdata.persistent_data.blocked_phrases, appdata.persistent_data.blocked_phrases_censor_char);
     }
 
     siv.call_on_name(
@@ -648,7 +642,7 @@ pub fn setup_ui(siv: &mut Cursive) -> LinearLayout {
                             appdata.persistent_data.current_channel = input.to_string();
                         });
 
-                        save_appdata(siv);
+                        save_appdata(get_appdata(siv));
                         visual_update(siv);
                         keybind_close_manual_end(siv, false);
                     } else {
@@ -778,7 +772,7 @@ pub fn setup_ui(siv: &mut Cursive) -> LinearLayout {
                             appdata.persistent_data.username = input.to_string();
                         });
 
-                        save_appdata(siv);
+                        save_appdata(get_appdata(siv));
                         visual_update(siv);
                         keybind_close_manual_end(siv, false);
                     } else {
@@ -835,7 +829,7 @@ pub fn setup_ui(siv: &mut Cursive) -> LinearLayout {
                         view.get_mut().remove_item(idx);
                     });
 
-                    save_appdata(siv);
+                    save_appdata(get_appdata(siv));
                     visual_update(siv);
                 });
             wordslist_view.add_all_str(&appdata.persistent_data.blocked_phrases);
@@ -867,7 +861,7 @@ pub fn setup_ui(siv: &mut Cursive) -> LinearLayout {
                     },
                 );
 
-                save_appdata(siv);
+                save_appdata(get_appdata(siv));
                 visual_update(siv);
             });
 
@@ -984,7 +978,7 @@ pub fn setup_ui(siv: &mut Cursive) -> LinearLayout {
                         }
                     });
 
-                    save_appdata(siv);
+                    save_appdata(get_appdata(siv));
                     visual_update(siv);
                     keybind_close_manual_end(siv, false);
                 })

+ 10 - 0
client_shared/Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "client_shared"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+logging = { path = "../logging" }
+utils = { path = "../utils" }
+
+url = "2.5.8"

+ 0 - 0
client/assets/logo.txt → client_shared/assets/logo.txt


+ 21 - 0
client_shared/src/lib.rs

@@ -0,0 +1,21 @@
+pub mod message;
+pub mod persistence;
+pub mod ui;
+pub mod utils;
+
+pub const MAX_MESSAGE_LENGTH: usize = 512;
+pub const MAX_USERNAME_LENGTH: usize = 16;
+pub const DEFAULT_USERNAME_PREFIX: &str = "Myst";
+pub const DEFAULT_CHANNEL: &str = "Root";
+pub const DEFAULT_PASSWORD: &str = "null";
+
+pub const API_CONNECTION_TIMEOUT: u64 = 2;
+pub const API_DEFAULT_PORT: u16 = 13337;
+
+pub const INVALID_MESSAGE_IDENT: &str = "ERR";
+
+pub const LOGO: &str = include_str!("../assets/logo.txt");
+pub const LOGO_DURATION: u128 = 500;
+
+pub const SAVE_FILE: &str = "savedata.bin";
+pub const SAVE_FILE_FUZZY: u64 = 0b0110110001101001011001110110110101100001001000000101100001000100;

+ 2 - 5
client/src/message.rs → client_shared/src/message.rs

@@ -4,7 +4,7 @@ use utils::{
     time::time_millis,
 };
 
-use crate::{INVALID_MESSAGE_IDENT, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH, get_appdata};
+use crate::{INVALID_MESSAGE_IDENT, MAX_MESSAGE_LENGTH, MAX_USERNAME_LENGTH};
 
 #[derive(Debug, Clone)]
 pub struct Message {
@@ -102,10 +102,7 @@ pub struct MessageSanitized {
 }
 
 impl MessageSanitized {
-    pub fn remove_blocked_phrases(&mut self, siv: &mut cursive::Cursive) {
-        let appdata = get_appdata(siv);
-        let wordlist = appdata.persistent_data.blocked_phrases;
-        let censor_char = appdata.persistent_data.blocked_phrases_censor_char;
+    pub fn remove_blocked_phrases(&mut self, wordlist: &[String], censor_char: char) {
         self.sender = utils::strings::remove_bad_words(&self.sender, &wordlist, censor_char);
         self.content = utils::strings::remove_bad_words(&self.content, &wordlist, censor_char);
         self.channel = utils::strings::remove_bad_words(&self.channel, &wordlist, censor_char);

+ 67 - 1
client/src/persistence.rs → client_shared/src/persistence.rs

@@ -1,6 +1,37 @@
+use std::{collections::HashMap, io::{Read as _, Write as _}};
+
 use utils::serialize::{DeserializationError, Serialize, SetOfSerializedObjects};
 
-use crate::{Appdata, DEFAULT_CHANNEL, DEFAULT_PASSWORD, DEFAULT_USERNAME_PREFIX, ui};
+use crate::{DEFAULT_CHANNEL, DEFAULT_PASSWORD, DEFAULT_USERNAME_PREFIX, SAVE_FILE, SAVE_FILE_FUZZY, message::Message, ui};
+
+
+#[derive(Clone)]
+pub struct Appdata {
+    pub persistent_data: Savedata,
+
+    pub messages: HashMap<String, Message>,
+    pub quick_close_window_count: usize,
+    pub local_channels: Vec<String>,
+    pub intro_screen_shown_until: u128,
+}
+
+impl Default for Appdata {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Appdata {
+    pub fn new() -> Self {
+        Self {
+            messages: HashMap::new(),
+            quick_close_window_count: 0,
+            local_channels: vec![DEFAULT_CHANNEL.to_string()],
+            intro_screen_shown_until: 0,
+            persistent_data: Savedata::new(),
+        }
+    }
+}
 
 #[derive(Clone)]
 pub struct Savedata {
@@ -136,3 +167,38 @@ impl Serialize for Savedata {
         })
     }
 }
+
+pub fn save_appdata(appdata: Appdata) {
+    let savedata = Savedata::from(appdata);
+    let bytes = savedata.serialize_checked();
+    let fuzzy_bytes = utils::binary::fuzzy_bytes(bytes, SAVE_FILE_FUZZY);
+
+    let mut file = std::fs::File::create(SAVE_FILE).expect("Failed to create savedata file.");
+    file.write_all(&fuzzy_bytes)
+        .expect("Failed to write savedata file.");
+
+    logging::trace("Wrote savadata to file.");
+}
+
+pub fn load_appdata() -> std::io::Result<Option<Appdata>> {
+    let mut df = logging::warn_deferred("Savedata file not found; using defaults");
+
+    let mut file = std::fs::File::open(SAVE_FILE)?;
+
+    let mut bytes = Vec::new();
+    file.read_to_end(&mut bytes)?;
+    let useful_bytes = utils::binary::fuzzy_bytes(bytes, SAVE_FILE_FUZZY);
+
+    df.cancel();
+
+    let Ok(savedata) = Savedata::deserialize_checked(useful_bytes) else {
+        // If the file is corrupted, create a new one
+        logging::warn("Savedata file corrupted; using defaults.");
+        return Ok(None);
+    };
+
+    let appdata = Appdata::from(savedata);
+
+    logging::trace("Savedata file loaded.");
+    Ok(Some(appdata))
+}

+ 7 - 0
client_shared/src/ui.rs

@@ -0,0 +1,7 @@
+
+#[derive(Debug, Clone, Copy)]
+pub enum Language {
+    English,
+    Dutch,
+    Japanese,
+}

+ 33 - 0
client_shared/src/utils.rs

@@ -0,0 +1,33 @@
+
+use std::str::FromStr as _;
+
+use url::Url;
+
+use crate::API_DEFAULT_PORT;
+
+pub enum UrlError {
+    ParseError(url::ParseError),
+    DomainBasePortError,
+}
+
+pub fn fix_url(url: &str) -> Result<String, UrlError> {
+    let mut url: String = url.to_string();
+
+    if !url.starts_with("http://") && !url.starts_with("https://") {
+        url = format!("http://{}", url);
+    }
+
+    let mut url = Url::from_str(&url).map_err(|x| UrlError::ParseError(x))?;
+
+    if url.port().is_none() {
+        url.set_port(Some(API_DEFAULT_PORT)).map_err(|_| UrlError::DomainBasePortError)?;
+    }
+
+    let mut url = url.to_string();
+
+    if !url.ends_with('/') {
+        url.push('/');
+    }
+
+    Ok(url)
+}

+ 33 - 9
logging/src/lib.rs

@@ -1,6 +1,7 @@
-use std::sync::Mutex;
+use std::{ffi::OsString, fs::File, io::{self, Write}, sync::Mutex};
 
 static LOG_LEVEL: Mutex<LogLevel> = Mutex::new(LogLevel::Info);
+static LOG_FILE: Mutex<Option<OsString>> = Mutex::new(None);
 
 pub use procmacro::log;
 
@@ -42,8 +43,22 @@ impl Drop for DeferredLog {
     }
 }
 
+fn write(line: &str) -> io::Result<()> {
+    let Some(path) = get_global_log_path() else {
+        println!("{}", line);
+        return Ok(());
+    };
+
+    let mut fio = File::options().append(true).create(true).open(path)?;
+    fio.write_all(line.as_bytes())?;
+    fio.write_all("\n".as_bytes())?;
+    fio.flush()?;
+
+    Ok(())
+}
+
 fn create_log(message: &str, level: LogLevel) {
-    let log_id = utils::rng::random_numeric_string(3);
+    let log_id = utils::rng::random_numeric_string(24);
 
     #[rustfmt::skip]
     let prefix = match level {
@@ -53,19 +68,19 @@ fn create_log(message: &str, level: LogLevel) {
         LogLevel::Info    => "[  INFO   ",
         LogLevel::Debug   => "[  DEBUG  ",
         LogLevel::Trace   => "[  TRACE  ",
-    }.to_string() + &format!("{:0>3}", log_id) + " ]";
+    }.to_string() + &utils::time::time_millis().to_string() + &format!("  {:0>24}", log_id) + " ]";
 
-    print!("{}", prefix);
+    write(&format!("{}", prefix)).expect("Failed to write log.");
 
     for (idx, line) in message.split("\n").enumerate() {
         if idx == 0 {
-            println!(" {line}");
+            write(&format!(" {line}")).expect("Failed to write log.");
         } else {
-            println!(" {idx:>7}  {log_id:0>3} | {line}");
+            write(&format!(" {idx:>7}  {log_id:0>3} | {line}")).expect("Failed to write log.");
         }
     }
 
-    println!();
+    write("").expect("Failed to write log.");
 }
 
 macro_rules! logger_setup {
@@ -90,12 +105,21 @@ logger_setup! { error, error_deferred, LogLevel::Error }
 logger_setup! { fatal, fatal_deferred, LogLevel::Fatal }
 
 pub fn set_global_log_level(level: LogLevel) {
-    let mut guard = LOG_LEVEL.lock().expect("Logging mutex is poisoned.");
+    let mut guard = LOG_LEVEL.lock().expect("Logging level mutex is poisoned.");
     *guard = level;
 }
 
 pub fn get_global_log_level() -> LogLevel {
-    *LOG_LEVEL.lock().expect("Logging mutex is poisoned.")
+    *LOG_LEVEL.lock().expect("Logging level mutex is poisoned.")
+}
+
+pub fn set_global_log_path(path: Option<OsString>) {
+    let mut guard = LOG_FILE.lock().expect("Logging path mutex is poisoned.");
+    *guard = path;
+}
+
+pub fn get_global_log_path() -> Option<OsString> {
+    LOG_FILE.lock().expect("Logging path mutex is poisoned.").clone()
 }
 
 #[cfg(test)]