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:
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(¶ms)
- .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(¶ms).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(¶ms.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>,