bhcli

"Strange's fork of n0tr1v's bhcli (onion)"
git clone https://git.dasho.dev/Strange/bhcli.git
Log | Files | Refs | README

commit ce0c50349a1f7d23fecf877e2cb46e766370a810
parent 1016e15d97f837d615d606946acae321dc2fbe9a
Author: Strange <StrangeGuy6228@protonmail.com>
Date:   Wed,  1 Oct 2025 12:51:42 +0530

updated to work with msg div without <input> tags

Diffstat:
MCargo.lock | 17++++++++++++++++-
MCargo.toml | 1+
Msrc/main.rs | 618++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/util/mod.rs | 1+
4 files changed, 313 insertions(+), 324 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -221,6 +221,7 @@ dependencies = [ "serde_json", "termage", "textwrap 0.16.1", + "tokio", "toml 0.7.8", "tui", "unicode-width", @@ -2801,12 +2802,26 @@ dependencies = [ "bytes", "libc", "mio 1.0.2", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", + "tokio-macros", "windows-sys 0.52.0", ] [[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2 1.0.86", + "quote 1.0.37", + "syn 2.0.77", +] + +[[package]] name = "tokio-native-tls" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -39,3 +39,4 @@ textwrap = "0.16.0" toml = "0.7.3" tui = { version = "0.19.0", features = ["crossterm"], default-features = false } unicode-width = "0.1.8" +tokio = { version = "1", features = ["full"] } diff --git a/src/main.rs b/src/main.rs @@ -4,7 +4,7 @@ mod util; use crate::lechatphp::LoginErr; use anyhow::{anyhow, Context}; -use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; +use chrono::{Datelike, NaiveDateTime, Utc}; use clap::Parser; use clipboard::ClipboardContext; use clipboard::ClipboardProvider; @@ -42,6 +42,7 @@ use std::sync::{Arc, MutexGuard}; use std::thread; use std::time::Duration; use std::time::Instant; +use tokio::process::Command as TokioCommand; use tui::layout::Rect; use tui::style::Color as tuiColor; use tui::{ @@ -61,7 +62,6 @@ const SEND_TO_MEMBERS: &str = "s ?"; const SEND_TO_STAFFS: &str = "s %"; const SEND_TO_ADMINS: &str = "s _"; const SOUND1: &[u8] = include_bytes!("sound1.mp3"); -const DKF_URL: &str = "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion"; const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down"; const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down"; const KICKED_ERR: &str = "You have been kicked"; @@ -71,7 +71,6 @@ const CAPTCHA_WG_ERR: &str = "Wrong Captcha"; const CAPTCHA_FAILED_SOLVE_ERR: &str = "Failed solve captcha"; const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out"; const UNKNOWN_ERR: &str = "Unknown error"; -const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion"; lazy_static! { static ref META_REFRESH_RGX: Regex = Regex::new(r#"url='([^']+)'"#).unwrap(); @@ -109,10 +108,24 @@ struct Profile { keepalive_send_to: String, } +#[derive(Debug, Serialize, Deserialize)] +struct LogLevel { + level: String, +} + +impl Default for LogLevel { + fn default() -> Self { + LogLevel { + level: "error".to_string(), + } + } +} + #[derive(Default, Debug, Serialize, Deserialize)] struct MyConfig { - dkf_api_key: Option<String>, profiles: HashMap<String, Profile>, + log_level: LogLevel, + commands: HashMap<String, String>, } #[derive(Parser)] @@ -162,7 +175,7 @@ struct Opts { profile: String, //Strange - #[arg(long,default_value = "0")] + #[arg(long, default_value = "0")] keepalive_send_to: Option<String>, #[arg(long)] @@ -170,6 +183,9 @@ struct Opts { #[arg(long)] sxiv: bool, + + #[arg(short = 'l', long, env = "LOG_LEVEL")] + log_level: Option<String>, } struct LeChatPHPConfig { @@ -296,8 +312,11 @@ impl LeChatPHPClient { let send_to = self.config.keepalive_send_to.clone(); thread::spawn(move || loop { let clb = || { - tx.send(PostType::Post("<keepalive>".to_owned(), Some(send_to.clone()))) - .unwrap(); + tx.send(PostType::Post( + "<keepalive>".to_owned(), + Some(send_to.clone()), + )) + .unwrap(); tx.send(PostType::DeleteLast).unwrap(); }; let timeout = after(Duration::from_secs(60 * 75)); @@ -365,39 +384,55 @@ impl LeChatPHPClient { let exit_rx = sig.lock().unwrap().clone(); let sig = Arc::clone(sig); let members_tag = self.config.members_tag.clone(); - thread::spawn(move || loop { - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); - let mut should_notify = false; - - if let Err(err) = get_msgs( - &client, - &base_url, - &page_php, - &session, - &username, - &users, - &sig, - &messages_updated_tx, - &members_tag, - &datetime_fmt, - &messages, - &mut should_notify, - ) { - log::error!("{}", err); - }; - let muted = { *is_muted.lock().unwrap() }; - if should_notify && !muted { - if let Err(err) = stream_handle.play_raw(source.convert_samples()) { + use std::panic::{catch_unwind, AssertUnwindSafe}; + + thread::spawn(move || { + let result = catch_unwind(AssertUnwindSafe(|| loop { + let (_stream, stream_handle) = OutputStream::try_default().unwrap(); + let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); + let mut should_notify = false; + + if let Err(err) = get_msgs( + &client, + &base_url, + &page_php, + &session, + &username, + &users, + &sig, + &messages_updated_tx, + &members_tag, + &datetime_fmt, + &messages, + &mut should_notify, + ) { log::error!("{}", err); } - } - let timeout = after(Duration::from_secs(refresh_rate)); - select! { - recv(&exit_rx) -> _ => return, - recv(&timeout) -> _ => {}, + let muted = { *is_muted.lock().unwrap() }; + if should_notify && !muted { + if let Err(err) = stream_handle.play_raw(source.convert_samples()) { + log::error!("{}", err); + } + } + + let timeout = after(Duration::from_secs(refresh_rate)); + select! { + recv(&exit_rx) -> _ => return, + recv(&timeout) -> _ => {}, + } + })); + + if let Err(panic_err) = result { + log::error!("❌ get_msgs thread panicked!"); + if let Some(msg) = panic_err.downcast_ref::<&str>() { + log::error!("panic message: {}", msg); + } else if let Some(msg) = panic_err.downcast_ref::<String>() { + log::error!("panic message: {}", msg); + } else { + log::error!("Unknown panic type"); + } } }) } @@ -495,7 +530,7 @@ impl LeChatPHPClient { fn login(&mut self) -> Result<(), LoginErr> { // If we provided a session, skip login process if self.session.is_some() { - // println!("Session in params: {:?}", self.session); + // println!("Session in params: {:?}", self.session); return Ok(()); } // println!("self.session is not Some"); @@ -667,7 +702,7 @@ impl LeChatPHPClient { code: KeyCode::Char('J'), modifiers: KeyModifiers::SHIFT, .. - } => self.handle_normal_mode_key_event_j(app,5), + } => self.handle_normal_mode_key_event_j(app, 5), KeyEvent { code: KeyCode::Char('k'), modifiers: KeyModifiers::NONE, @@ -682,7 +717,7 @@ impl LeChatPHPClient { code: KeyCode::Char('K'), modifiers: KeyModifiers::SHIFT, .. - } => self.handle_normal_mode_key_event_k(app,5), + } => self.handle_normal_mode_key_event_k(app, 5), KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, @@ -709,30 +744,59 @@ impl LeChatPHPClient { .. } => self.handle_normal_mode_key_event_yank_link(app), - //Strange KeyEvent { - code: KeyCode::Char('D'), - modifiers: KeyModifiers::SHIFT, + code: KeyCode::Char('s'), + modifiers: KeyModifiers::NONE, .. - } => self.handle_normal_mode_key_event_download_link(app), + } => { + let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); + if let Some(session) = &self.session { + ctx.set_contents(session.clone()).unwrap(); + } else { + log::error!("Session was empty !") + } + } //Strange KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::NONE, .. - } => self.handle_normal_mode_key_event_download_and_view(app), - - // KeyEvent { - // code: KeyCode::Char('d'), - // modifiers: KeyModifiers::NONE, - // .. - // } => self.handle_normal_mode_key_event_debug(app), - // KeyEvent { - // code: KeyCode::Char('D'), - // modifiers: KeyModifiers::SHIFT, - // .. - // } => self.handle_normal_mode_key_event_debug2(app), + } => { + let mut app = app.clone(); + let config_url = self.config.url.clone(); + let members_tag = self.config.members_tag.clone(); + log::info!("Calling handle_normal_mode_key_event_download_link"); + tokio::spawn(async move { + Self::handle_normal_mode_key_event_download_link( + &mut app, + config_url, + members_tag, + false, + ) + .await; + }); + } + KeyEvent { + code: KeyCode::Char('D'), + modifiers: KeyModifiers::SHIFT, + .. + } => { + let mut app = app.clone(); + let config_url = self.config.url.clone(); + let members_tag = self.config.members_tag.clone(); + log::info!("Calling handle_normal_mode_key_event_download_link"); + tokio::spawn(async move { + Self::handle_normal_mode_key_event_download_link( + &mut app, + config_url, + members_tag, + true, + ) + .await; + }); + } + KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::NONE, @@ -797,7 +861,9 @@ impl LeChatPHPClient { code: KeyCode::Char('T'), modifiers: KeyModifiers::SHIFT, .. - } => self.handle_normal_mode_key_event_translate(app, messages), + } => { + self.handle_normal_mode_key_event_translate(app, messages); + } KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::CONTROL, @@ -1074,93 +1140,109 @@ impl LeChatPHPClient { } //Strange - fn handle_normal_mode_key_event_download_link(&mut self, app: &mut App) { + async fn handle_normal_mode_key_event_download_link( + app: &mut App, + config_url: String, + members_tag: String, + show_image: bool, + ) { + log::debug!("handle_normal_mode_key_event_download_link called"); if let Some(idx) = app.items.state.selected() { if let Some(item) = app.items.items.get(idx) { if let Some(upload_link) = &item.upload_link { - let url = format!("{}{}", self.config.url, upload_link); - let _ = Command::new("curl") - .args([ - "--socks5", - "localhost:9050", - "--socks5-hostname", - "localhost:9050", - &url, - ]) - .arg("-o") - .arg("download.img") - .output() - .expect("Failed to execute curl command"); - } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag) - { - let finder = LinkFinder::new(); - let links: Vec<_> = finder.links(msg.as_str()).collect(); - if let Some(link) = links.first() { - let url = link.as_str(); - let _ = Command::new("curl") - .args([ - "--socks5", - "localhost:9050", - "--socks5-hostname", - "localhost:9050", - url, - ]) - .arg("-o") - .arg("download.img") - .output() - .expect("Failed to execute curl command"); - } - } - } - } - } + let url = format!("{}{}", config_url, upload_link); + let filename = if let Some((username, _, full_text)) = + get_message(&item.text, &members_tag) + { + if let Some(start) = full_text.find('[') { + if let Some(end) = full_text[start..].find(']') { + let inside_brackets = &full_text[start + 1..start + end]; + let sanitized = inside_brackets.replace(' ', "_"); + format!("{}_{}", username, sanitized) + } else { + item.text.text() + } + } else { + item.text.text() + } + } else { + item.text.text() + }; - //strageEdit - fn handle_normal_mode_key_event_download_and_view(&mut self, app: &mut App) { - if let Some(idx) = app.items.state.selected() { - if let Some(item) = app.items.items.get(idx) { - if let Some(upload_link) = &item.upload_link { - let url = format!("{}{}", self.config.url, upload_link); - let _ = Command::new("curl") + let output = TokioCommand::new("curl") .args([ "--socks5", "localhost:9050", "--socks5-hostname", "localhost:9050", &url, + "-o", + &filename, ]) - .arg("-o") - .arg("download.img") .output() - .expect("Failed to execute curl command"); + .await; + + match output { + Ok(output) => { + if output.status.success() { + if show_image { + let _ = + TokioCommand::new("sxiv").arg("-a").arg(&filename).spawn(); + } + } else { + log::error!("Curl command failed with status: {:?}", output.status); + log::error!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + } + } + Err(e) => { + log::error!("Failed to execute curl command: {}", e); + } + } + } else { + let msg = if let Some((_, _, msg)) = get_message(&item.text, &members_tag) { + msg.to_string() + } else { + item.text.text() + }; - let _ = Command::new("xdg-open") - .arg("./download.img") - .output() - .expect("Failed to execute sxiv command"); - } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag) - { let finder = LinkFinder::new(); let links: Vec<_> = finder.links(msg.as_str()).collect(); if let Some(link) = links.first() { let url = link.as_str(); - let _ = Command::new("curl") + let output = TokioCommand::new("curl") .args([ "--socks5", "localhost:9050", "--socks5-hostname", "localhost:9050", url, + "-O", ]) - .arg("-o") - .arg("download.img") .output() - .expect("Failed to execute curl command"); - - let _ = Command::new("sxiv") - .arg("./download.img") - .output() - .expect("Failed to execute sxiv command"); + .await; + + match output { + Ok(output) => { + if !output.status.success() { + log::error!( + "Curl command failed with status: {:?}", + output.status + ); + log::error!( + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + log::error!( + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + } + Err(e) => { + log::error!("Failed to execute curl command: {}", e); + } + } } } } @@ -1264,28 +1346,30 @@ impl LeChatPHPClient { app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, ) { - log::error!("translate running"); + log::info!("translate running"); if let Some(idx) = app.items.state.selected() { - log::error!("1353"); let mut message_lock = messages.lock().unwrap(); if let Some(message) = message_lock.get_mut(idx) { - log::error!("1356"); - let original_text = &mut message.text; - let output = Command::new("trans") - .arg("-b") - .arg(&original_text.text()) - .output() - .expect("Failed to execute translation command"); - - if output.status.success() { - if let Ok(new_text) = String::from_utf8(output.stdout) { - *original_text = StyledText::Text(new_text.trim().to_owned()); - log::error!("Translation successful: {}", new_text); - } else { - log::error!("Failed to decode translation output as UTF-8"); + let input_text = message.text.text(); // convert StyledText -> &str + let output = Command::new("trans").arg("-b").arg(input_text).output(); + + match output { + Ok(output) => { + if output.status.success() { + if let Ok(new_text) = String::from_utf8(output.stdout) { + message.text = StyledText::Text(new_text.trim().to_owned()); + log::info!("Translation successful: {}", new_text); + } else { + log::error!("Failed to decode translation output as UTF-8"); + } + } else { + log::error!("Translation command failed: {:?}", output.status); + log::error!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + } + } + Err(e) => { + log::error!("Failed to execute translation command: {}", e); } - } else { - log::error!("Translation command failed with error: {:?}", output.status); } } } @@ -2123,6 +2207,7 @@ struct Params { max_login_retry: isize, keepalive_send_to: Option<String>, session: Option<String>, + log_level: String, } #[derive(Clone)] @@ -2229,90 +2314,7 @@ fn ask_password(password: Option<String>) -> String { password.unwrap_or_else(|| rpassword::prompt_password("Password: ").unwrap()) } -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DkfNotifierResp { - #[serde(rename = "NewMessageSound")] - pub new_message_sound: bool, - #[serde(rename = "TaggedSound")] - pub tagged_sound: bool, - #[serde(rename = "PmSound")] - pub pm_sound: bool, - #[serde(rename = "InboxCount")] - pub inbox_count: i64, - #[serde(rename = "LastMessageCreatedAt")] - pub last_message_created_at: String, -} - -fn start_dkf_notifier(client: &Client, dkf_api_key: &str) { - let client = client.clone(); - let dkf_api_key = dkf_api_key.to_owned(); - let mut last_known_date = Utc::now(); - thread::spawn(move || loop { - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); - - let params: Vec<(&str, String)> = vec![( - "last_known_date", - last_known_date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true), - )]; - let right_url = format!("{}/api/v1/chat/1/notifier", DKF_URL); - if let Ok(resp) = client - .post(right_url) - .form(&params) - .header("DKF_API_KEY", &dkf_api_key) - .send() - { - if let Ok(txt) = resp.text() { - if let Ok(v) = serde_json::from_str::<DkfNotifierResp>(&txt) { - if v.pm_sound || v.tagged_sound { - stream_handle.play_raw(source.convert_samples()).unwrap(); - } - last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at) - .unwrap() - .with_timezone(&Utc); - } - } - } - thread::sleep(Duration::from_secs(5)); - }); -} - -// Start thread that looks for new emails on DNMX every minutes. -fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) { - let params: Vec<(&str, &str)> = vec![("login_username", username), ("secretkey", password)]; - let login_url = format!("{}/src/redirect.php", DNMX_URL); - client.post(login_url).form(&params).send().unwrap(); - - let client_clone = client.clone(); - thread::spawn(move || loop { - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); - - let right_url = format!("{}/src/right_main.php", DNMX_URL); - if let Ok(resp) = client_clone.get(right_url).send() { - let mut nb_mails = 0; - let doc = Document::from(resp.text().unwrap().as_str()); - if let Some(table) = doc.find(Name("table")).nth(7) { - table.find(Name("tr")).skip(1).for_each(|n| { - if let Some(td) = n.find(Name("td")).nth(2) { - if td.find(Name("b")).nth(0).is_some() { - nb_mails += 1; - } - } - }); - } - if nb_mails > 0 { - log::error!("{} new mails", nb_mails); - stream_handle.play_raw(source.convert_samples()).unwrap(); - } - } - thread::sleep(Duration::from_secs(60)); - }); -} - -//Strange -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] struct Commands { commands: HashMap<String, String>, } @@ -2325,93 +2327,83 @@ impl Default for Commands { } } -// Strange -// Function to read the configuration file and parse it -fn read_commands_file(file_path: &str) -> Result<Commands, Box<dyn std::error::Error>> { - // Read the contents of the file - let commands_content = std::fs::read_to_string(file_path)?; - // log::error!("Read file contents: {}", commands_content); - // Deserialize the contents into a Commands struct - let commands: Commands = toml::from_str(&commands_content)?; - // log::error!( - // "Deserialized file contents into Commands struct: {:?}", - // commands - // ); - - Ok(commands) -} - -fn main() -> anyhow::Result<()> { - let mut opts: Opts = Opts::parse(); - // println!("Parsed Session: {:?}", opts.session); - - - // Configs file - if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) { - println!("Config path: {:?}", config_path); - } - if let Ok(cfg) = confy::load::<MyConfig>("bhcli", None) { - if opts.dkf_api_key.is_none() { - opts.dkf_api_key = cfg.dkf_api_key; - } - if let Some(default_profile) = cfg.profiles.get(&opts.profile) { - if opts.username.is_none() { - opts.username = Some(default_profile.username.clone()); - opts.password = Some(default_profile.password.clone()); - } - } - } +fn parse_params() -> anyhow::Result<Params> { + let opts = Opts::parse(); + let cfg = confy::load::<MyConfig>("bhcli", None).unwrap_or_default(); - let logfile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}"))) - .build("bhcli.log")?; + let profile = cfg.profiles.get(&opts.profile); - let config = log4rs::config::Config::builder() - .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile))) - .build( - log4rs::config::Root::builder() - .appender("logfile") - .build(LevelFilter::Error), - )?; + let username = opts + .username + .clone() + .or_else(|| profile.map(|p| p.username.clone())); + let password = opts + .password + .clone() + .or_else(|| profile.map(|p| p.password.clone())); - log4rs::init_config(config)?; + let log_level = opts + .log_level + .clone() + .or(Some(cfg.log_level.level)) + .unwrap_or_else(|| "error".to_string()); let client = get_tor_client(&opts.socks_proxy_url, opts.no_proxy); - - // If dnmx username is set, start mail notifier thread - if let Some(dnmx_username) = opts.dnmx_username { - start_dnmx_mail_notifier(&client, &dnmx_username, &opts.dnmx_password.unwrap()) - } - - if let Some(dkf_api_key) = &opts.dkf_api_key { - start_dkf_notifier(&client, dkf_api_key); - } - let guest_color = get_guest_color(opts.guest_color); - let username = ask_username(opts.username); - let password = ask_password(opts.password); + let final_username = ask_username(username); + let final_password = ask_password(password); - let params = Params { + Ok(Params { url: opts.url, page_php: opts.page_php, datetime_fmt: opts.datetime_fmt, members_tag: opts.members_tag, - username, - password, + username: final_username, + password: final_password, guest_color, - client: client.clone(), + client, manual_captcha: opts.manual_captcha, sxiv: opts.sxiv, refresh_rate: opts.refresh_rate, max_login_retry: opts.max_login_retry, keepalive_send_to: opts.keepalive_send_to, - session: opts.session.clone(), + session: opts.session, + log_level, // now just a String + }) +} + +fn setup_logging(level: &str) -> anyhow::Result<()> { + let level_filter = match level.to_lowercase().as_str() { + "off" => LevelFilter::Off, + "error" => LevelFilter::Error, + "warn" => LevelFilter::Warn, + "info" => LevelFilter::Info, + "debug" => LevelFilter::Debug, + "trace" => LevelFilter::Trace, + _ => LevelFilter::Error, }; - // println!("Session[2378]: {:?}", opts.session); + let logfile = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}"))) + .build("bhcli.log")?; - ChatClient::new(params).run_forever(); + let config = log4rs::config::Config::builder() + .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile))) + .build( + log4rs::config::Root::builder() + .appender("logfile") + .build(level_filter), + )?; + + log4rs::init_config(config)?; + Ok(()) +} +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let params = parse_params()?; + setup_logging(&params.log_level)?; + ChatClient::new(params).run_forever(); Ok(()) } @@ -2742,9 +2734,16 @@ fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> { .find(Attr("class", "msg")) .filter_map(|tag| { let mut id: Option<usize> = None; + // if let Some(checkbox) = tag.find(Name("input")).next() { + // let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap(); + // id = Some(id_value); + // } if let Some(checkbox) = tag.find(Name("input")).next() { - let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap(); - id = Some(id_value); + if let Some(val) = checkbox.attr("value") { + if let Ok(id_value) = val.parse::<usize>() { + id = Some(id_value); + } + } } if let Some(date_node) = tag.find(Name("small")).next() { if let Some(msg_span) = tag.find(Name("span")).next() { @@ -2838,11 +2837,12 @@ fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: &str) -> Vec<Vec<(tuiC if let Some(valid_slice) = txt.get(0..remain) { line.push((color, valid_slice.to_owned())); } else { - let valid_remain = txt.char_indices() + let valid_remain = txt + .char_indices() .take_while(|&(i, _)| i < remain) .last() .map(|(i, _)| i) - .unwrap_or(txt.len()); + .unwrap_or(txt.len()); line.push((color, txt[..valid_remain].to_owned())); } @@ -2854,7 +2854,8 @@ fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: &str) -> Vec<Vec<(tuiC if let Some(valid_slice) = txt.get(remain..) { ctxt.push((color, valid_slice.to_owned())); } else { - let valid_remain = txt.char_indices() + let valid_remain = txt + .char_indices() .skip_while(|&(i, _)| i < remain) // Find first valid boundary after remain .map(|(i, _)| i) .next() @@ -3144,7 +3145,7 @@ fn random_string(n: usize) -> String { std::str::from_utf8(&s).unwrap().to_owned() } -#[derive(PartialEq)] +#[derive(PartialEq, Clone)] enum InputMode { LongMessage, Normal, @@ -3153,6 +3154,7 @@ enum InputMode { } /// App holds the state of the application +#[derive(Clone)] struct App { /// Current value of the input box input: String, @@ -3174,36 +3176,6 @@ struct App { impl Default for App { fn default() -> App { - // Read commands from the file and set them as default values - let commands = if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) { - if let Some(config_path_str) = config_path.to_str() { - match read_commands_file(config_path_str) { - Ok(commands) => commands, - Err(err) => { - log::error!( - "Failed to read commands from config file - {} : -{}", - config_path_str, - err - ); - Commands { - commands: HashMap::new(), - } - } - } - } else { - log::error!("Failed to convert configuration file path to string."); - Commands { - commands: HashMap::new(), - } - } - } else { - log::error!("Failed to get configuration file path."); - Commands { - commands: HashMap::new(), - } - }; - App { input: String::new(), input_idx: 0, @@ -3218,7 +3190,7 @@ impl Default for App { members_tag: "".to_owned(), staffs_tag: "".to_owned(), long_message: None, - commands, + commands: Commands::default(), } } } diff --git a/src/util/mod.rs b/src/util/mod.rs @@ -2,6 +2,7 @@ pub mod event; use tui::widgets::ListState; +#[derive(Clone)] pub struct StatefulList<T> { pub state: ListState, pub items: Vec<T>,