bhcli

A TUI for chatting on LE PHP Chats (onion)
git clone https://git.dasho.dev/n0tr1v/bhcli.git
Log | Files | Refs | README

commit 28441f89139fc9779a05d91952d2fb2a06659a69
parent 318fdcfbf22367520483db7a94f10e0debe4583e
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Thu,  6 Apr 2023 22:35:31 -0700

fix bhc client

Diffstat:
MCargo.lock | 1+
MCargo.toml | 1+
Msrc/lechatphp/mod.rs | 205++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/main.rs | 273++++++++++++++++---------------------------------------------------------------
4 files changed, 260 insertions(+), 220 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -119,6 +119,7 @@ checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" name = "bhcli" version = "0.1.0" dependencies = [ + "anyhow", "base64", "bresenham", "chrono", diff --git a/Cargo.toml b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow = "1.0.70" base64 = "0.21.0" bresenham = "0.1.1" chrono = "0.4.19" diff --git a/src/lechatphp/mod.rs b/src/lechatphp/mod.rs @@ -1 +1,202 @@ -pub mod captcha; -\ No newline at end of file +use std::{error, fs, io, thread}; +use std::fmt::{Display, Formatter}; +use std::io::Write; +use std::time::Duration; +use base64::Engine; +use base64::engine::general_purpose; +use http::StatusCode; +use regex::Regex; +use reqwest::blocking::Client; +use select::document::Document; +use select::predicate::{And, Attr, Name}; +use crate::{CAPTCHA_FAILED_SOLVE_ERR, CAPTCHA_USED_ERR, CAPTCHA_WG_ERR, KICKED_ERR, LANG, NICKNAME_ERR, REG_ERR, SERVER_DOWN_500_ERR, SERVER_DOWN_ERR, SESSION_RGX, trim_newline, UNKNOWN_ERR}; + +pub mod captcha; + +#[derive(Debug)] +pub enum LoginErr { + ServerDownErr, + ServerDown500Err, + CaptchaFailedSolveErr, + CaptchaUsedErr, + CaptchaWgErr, + RegErr, + NicknameErr, + KickedErr, + UnknownErr, + Reqwest(reqwest::Error), +} + +impl From<reqwest::Error> for LoginErr { + fn from(value: reqwest::Error) -> Self { + LoginErr::Reqwest(value) + } +} + +impl Display for LoginErr { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + LoginErr::ServerDownErr => SERVER_DOWN_ERR.to_owned(), + LoginErr::ServerDown500Err => SERVER_DOWN_500_ERR.to_owned(), + LoginErr::CaptchaFailedSolveErr => CAPTCHA_FAILED_SOLVE_ERR.to_owned(), + LoginErr::CaptchaUsedErr => CAPTCHA_USED_ERR.to_owned(), + LoginErr::CaptchaWgErr => CAPTCHA_WG_ERR.to_owned(), + LoginErr::RegErr => REG_ERR.to_owned(), + LoginErr::NicknameErr => NICKNAME_ERR.to_owned(), + LoginErr::KickedErr => KICKED_ERR.to_owned(), + LoginErr::UnknownErr => UNKNOWN_ERR.to_owned(), + LoginErr::Reqwest(e) => e.to_string(), + }; + write!(f, "{}", s) + } +} + +impl error::Error for LoginErr {} + +pub fn login( + client: &Client, + base_url: &str, + page_php: &str, + username: &str, + password: &str, + color: &str, + manual_captcha: bool) -> std::result::Result<String, LoginErr> +{ + // Get login page + let login_url = format!("{}/{}", &base_url, &page_php); + let resp = client.get(&login_url).send()?; + if resp.status() == StatusCode::BAD_GATEWAY { + return Err(LoginErr::ServerDownErr); + } + let resp = resp.text()?; + let doc = Document::from(resp.as_str()); + + // Post login form + let mut params = vec![ + ("action", "login".to_owned()), + ("lang", LANG.to_owned()), + ("nick", username.to_owned()), + ("pass", password.to_owned()), + ("colour", color.to_owned()), + ]; + + if let Some(captcha_node) = doc.find(And(Name("input"), Attr("name", "challenge"))).next() { + let captcha_value = captcha_node.attr("value").unwrap(); + let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); + + let mut captcha_input = String::new(); + if manual_captcha { + // Otherwise, save the captcha on disk and prompt user for answer + let img_decoded = general_purpose::STANDARD.decode(captcha_img.strip_prefix("data:image/gif;base64,").unwrap()).unwrap(); + let img = image::load_from_memory(&img_decoded).unwrap(); + let img_buf = image::imageops::resize( + &img, + img.width() * 4, + img.height() * 4, + image::imageops::FilterType::Nearest, + ); + // Save captcha as file on disk + img_buf.save("captcha.gif").unwrap(); + + termage::display_image("captcha.gif", img.width(), img.height()); + + // Enter captcha + print!("captcha: "); + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut captcha_input).unwrap(); + trim_newline(&mut captcha_input); + } else { + match captcha::solve_b64(captcha_img) { + Some(answer) => captcha_input = answer, + None => return Err(LoginErr::CaptchaFailedSolveErr), + } + } + + params.extend(vec![ + ("challenge", captcha_value.to_owned()), + ("captcha", captcha_input.clone()), + ]); + } + + let mut resp = client.post(&login_url).form(&params).send()?; + match resp.status() { + StatusCode::BAD_GATEWAY => return Err(LoginErr::ServerDownErr), + StatusCode::INTERNAL_SERVER_ERROR => return Err(LoginErr::ServerDown500Err), + _ => {} + } + + let mut refresh_header = resp.headers().get("refresh").map(|v| v.to_str().unwrap()).unwrap_or(""); + while refresh_header != "" { + let rgx = Regex::new(r#"URL=(.+)"#).unwrap(); + let refresh_url = format!("{}{}", base_url, rgx.captures(&refresh_header).unwrap().get(1).unwrap().as_str()); + println!("waitroom enabled, wait 10sec"); + thread::sleep(Duration::from_secs(10)); + resp = client.get(refresh_url.clone()).send()?; + refresh_header = resp.headers().get("refresh").map(|v| v.to_str().unwrap()).unwrap_or(""); + } + + let mut resp = resp.text()?; + if resp.contains(CAPTCHA_USED_ERR) { + return Err(LoginErr::CaptchaUsedErr); + } else if resp.contains(CAPTCHA_WG_ERR) { + return Err(LoginErr::CaptchaWgErr); + } else if resp.contains(REG_ERR) { + return Err(LoginErr::RegErr); + } else if resp.contains(NICKNAME_ERR) { + return Err(LoginErr::NicknameErr); + } else if resp.contains(KICKED_ERR) { + return Err(LoginErr::KickedErr); + } + + let mut doc = Document::from(resp.as_str()); + if let Some(body) = doc.find(Name("body")).next() { + if let Some(body_class) = body.attr("class") { + if body_class == "error" { + if let Some(h2) = doc.find(Name("h2")).next() { + log::error!("{}", h2.text()); + } + return Err(LoginErr::UnknownErr); + } else if body_class == "failednotice" { + log::error!("failed logins: {}", body.text()); + let nc = doc.find(Attr("name", "nc")).next().unwrap(); + let nc_value = nc.attr("value").unwrap().to_owned(); + let params: Vec<(&str, String)> = vec![ + ("lang", LANG.to_owned()), + ("nc", nc_value.to_owned()), + ("action", "login".to_owned()), + ]; + resp = client.post(&login_url).form(&params).send()?.text()?; + doc = Document::from(resp.as_str()); + } + } + } + + let iframe = match doc.find(Attr("name", "view")).next() { + Some(view) => view, + None => { + fs::write("./dump_login_err.html", resp.as_str()).unwrap(); + panic!("failed to get view iframe"); + } + }; + let iframe_src = iframe.attr("src").unwrap(); + + let session_captures = SESSION_RGX.captures(iframe_src).unwrap(); + let session = session_captures.get(1).unwrap().as_str(); + Ok(session.to_owned()) +} + +pub fn logout( + client: &Client, + base_url: &str, + page_php: &str, + session: &str) -> anyhow::Result<()> +{ + let full_url = format!("{}/{}", &base_url, &page_php); + let params = [ + ("action", "logout"), + ("session", &session), + ("lang", LANG), + ]; + client.post(&full_url).form(&params).send()?; + Ok(()) +} +\ No newline at end of file diff --git a/src/main.rs b/src/main.rs @@ -2,12 +2,12 @@ mod util; mod lechatphp; mod bhc; +use anyhow::anyhow; use log; use log::LevelFilter; use log4rs::append::file::FileAppender; use log4rs::encode::pattern::PatternEncoder; use log4rs; -use base64::{engine::general_purpose, Engine as _}; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; use clap::Parser; use clipboard::ClipboardContext; @@ -22,8 +22,6 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use http::StatusCode; -use image; use lazy_static::lazy_static; use linkify::LinkFinder; use rand::distributions::Alphanumeric; @@ -33,12 +31,9 @@ use reqwest::blocking::multipart; use reqwest::blocking::Client; use rodio::{source::Source, Decoder, OutputStream}; use select::document::Document; -use select::predicate::{And, Attr, Name}; +use select::predicate::{Attr, Name}; use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; -use std::error; -use std::fmt::{Display, Formatter}; -use std::fs; use std::io::Cursor; use std::io::{self, Write}; use std::sync::Arc; @@ -46,8 +41,9 @@ use std::sync::Mutex; use std::thread; use std::time::Duration; use std::time::Instant; +use http::StatusCode; use reqwest; -use termage; +use reqwest::redirect::Policy; use textwrap; use tui::layout::Rect; use tui::style::Color as tuiColor; @@ -61,6 +57,7 @@ use tui::{ }; use unicode_width::UnicodeWidthStr; use util::StatefulList; +use crate::lechatphp::LoginErr; const LANG: &str = "en"; const SEND_TO_ALL: &str = "s *"; @@ -86,8 +83,6 @@ const STUXNET: &str = "STUXNET"; const FAGGOT: &str = "faggot"; const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion"; -type Result<T> = std::result::Result<T, Box<dyn error::Error>>; - lazy_static! { static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap(); static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap(); @@ -103,18 +98,6 @@ lazy_static! { static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap(); } -#[derive(Debug, Serialize, Deserialize)] -enum Typ { - BHC, - Custom, -} - -impl Typ { - fn bhc() -> Self { - Typ::BHC - } -} - fn default_empty_str() -> String { "".to_string() } @@ -123,8 +106,6 @@ fn default_empty_str() -> String { struct Profile { username: String, password: String, - #[serde(default = "Typ::bhc")] - typ: Typ, #[serde(default = "default_empty_str")] url: String, #[serde(default = "default_empty_str")] @@ -203,7 +184,7 @@ impl LeChatPHPConfig { Self { url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(), datetime_fmt: "%m-%d %H:%M:%S".to_owned(), - page_php: "index.php".to_owned(), + page_php: "chat.php".to_owned(), keepalive_send_to: Some("0".to_owned()), members_tag: "[M] ".to_owned(), staffs_tag: "[Staff] ".to_owned(), @@ -222,52 +203,13 @@ impl LeChatPHPConfig { } } -#[derive(Debug)] -enum LoginErr { - ServerDownErr, - ServerDown500Err, - CaptchaFailedSolveErr, - CaptchaUsedErr, - CaptchaWgErr, - RegErr, - NicknameErr, - KickedErr, - UnknownErr, - Reqwest(reqwest::Error), -} - -impl From<reqwest::Error> for LoginErr { - fn from(value: reqwest::Error) -> Self { - LoginErr::Reqwest(value) - } -} - -impl Display for LoginErr { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let s = match self { - LoginErr::ServerDownErr => SERVER_DOWN_ERR.to_owned(), - LoginErr::ServerDown500Err => SERVER_DOWN_500_ERR.to_owned(), - LoginErr::CaptchaFailedSolveErr => CAPTCHA_FAILED_SOLVE_ERR.to_owned(), - LoginErr::CaptchaUsedErr => CAPTCHA_USED_ERR.to_owned(), - LoginErr::CaptchaWgErr => CAPTCHA_WG_ERR.to_owned(), - LoginErr::RegErr => REG_ERR.to_owned(), - LoginErr::NicknameErr => NICKNAME_ERR.to_owned(), - LoginErr::KickedErr => KICKED_ERR.to_owned(), - LoginErr::UnknownErr => UNKNOWN_ERR.to_owned(), - LoginErr::Reqwest(e) => e.to_string(), - }; - write!(f, "{}", s) - } -} - -impl error::Error for LoginErr {} - struct BaseClient { username: String, password: String, } struct LeChatPHPClient { + chat_type: ClientType, base_client: BaseClient, guest_color: String, client: Client, @@ -295,19 +237,19 @@ impl LeChatPHPClient { loop { if let Err(e) = self.login() { match e { - LoginErr::KickedErr | LoginErr::RegErr | LoginErr::NicknameErr | LoginErr::UnknownErr => { + lechatphp::LoginErr::KickedErr | lechatphp::LoginErr::RegErr | lechatphp::LoginErr::NicknameErr | lechatphp::LoginErr::UnknownErr => { log::error!("{}", e); break; }, - LoginErr::CaptchaFailedSolveErr => { + lechatphp::LoginErr::CaptchaFailedSolveErr => { log::error!("{}", e); continue; }, - LoginErr::CaptchaWgErr | LoginErr::CaptchaUsedErr => {}, - LoginErr::ServerDownErr | LoginErr::ServerDown500Err => { + lechatphp::LoginErr::CaptchaWgErr | lechatphp::LoginErr::CaptchaUsedErr => {}, + lechatphp::LoginErr::ServerDownErr | lechatphp::LoginErr::ServerDown500Err => { log::error!("{}", e); }, - LoginErr::Reqwest(err) => { + lechatphp::LoginErr::Reqwest(err) => { if err.is_connect() { log::error!("{}\nIs tor proxy enabled ?", err); break; @@ -712,7 +654,7 @@ impl LeChatPHPClient { h } - fn get_msgs(&mut self) -> Result<ExitSignal> { + fn get_msgs(&mut self) -> anyhow::Result<ExitSignal> { let terminate_signal: ExitSignal; let messages: Arc<Mutex<Vec<Message>>> = Arc::new(Mutex::new(Vec::new())); @@ -795,150 +737,43 @@ impl LeChatPHPClient { Ok(terminate_signal) } - fn post_msg(&self, post_type: PostType) -> Result<()> { + fn post_msg(&self, post_type: PostType) -> anyhow::Result<()> { self.tx.send(post_type)?; Ok(()) } - fn login(&mut self) -> std::result::Result<(), LoginErr> { + fn login(&mut self) -> Result<(), lechatphp::LoginErr> { // If we provided a session, skip login process if self.session != "" { return Ok(()); } - - // Get login page - let login_url = format!("{}/{}", &self.config.url, &self.config.page_php); - let resp = self.client.get(&login_url).send()?; - if resp.status() == StatusCode::BAD_GATEWAY { - return Err(LoginErr::ServerDownErr); - } - let resp = resp.text()?; - let doc = Document::from(resp.as_str()); - - // Post login form - let mut params = vec![ - ("action", "login".to_owned()), - ("lang", LANG.to_owned()), - ("nick", self.base_client.username.clone()), - ("pass", self.base_client.password.clone()), - ("colour", self.guest_color.clone()), - ]; - - if let Some(captcha_node) = doc.find(And(Name("input"), Attr("name", "challenge"))).next() { - let captcha_value = captcha_node.attr("value").unwrap(); - let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); - + if self.chat_type == ClientType::BHC { + let captcha_bytes = self.client.get(format!("{}/captcha.php", &self.config.url)).send().unwrap().bytes().unwrap(); + let img = image::load_from_memory(&captcha_bytes).unwrap(); + let img_buf = image::imageops::resize(&img, img.width(), img.height(), image::imageops::FilterType::Nearest); + // Save captcha as file on disk + img_buf.save("captcha.gif").unwrap(); + termage::display_image("captcha.gif", img.width(), img.height()); + // Enter captcha + print!("captcha: "); let mut captcha_input = String::new(); - if self.manual_captcha { - // Otherwise, save the captcha on disk and prompt user for answer - let img_decoded = general_purpose::STANDARD.decode(captcha_img.strip_prefix("data:image/gif;base64,").unwrap()).unwrap(); - let img = image::load_from_memory(&img_decoded).unwrap(); - let img_buf = image::imageops::resize( - &img, - img.width() * 4, - img.height() * 4, - image::imageops::FilterType::Nearest, - ); - // Save captcha as file on disk - img_buf.save("captcha.gif").unwrap(); - - termage::display_image("captcha.gif", img.width(), img.height()); - - // Enter captcha - print!("captcha: "); - io::stdout().flush().unwrap(); - io::stdin().read_line(&mut captcha_input).unwrap(); - trim_newline(&mut captcha_input); - } else { - match lechatphp::captcha::solve_b64(captcha_img) { - Some(answer) => captcha_input = answer, - None => return Err(LoginErr::CaptchaFailedSolveErr), - } + io::stdout().flush().unwrap(); + io::stdin().read_line(&mut captcha_input).unwrap(); + trim_newline(&mut captcha_input); + let params = vec![("input", captcha_input), ("submit", "Enter+Chat".to_owned())]; + let resp = self.client.post(format!("{}/form.php", &self.config.url)).form(&params).send().unwrap(); + if resp.status() != StatusCode::SEE_OTHER { + return Err(LoginErr::CaptchaWgErr); } - - params.extend(vec![ - ("challenge", captcha_value.to_owned()), - ("captcha", captcha_input.clone()), - ]); - } - - let mut resp = self.client.post(&login_url).form(&params).send()?; - match resp.status() { - StatusCode::BAD_GATEWAY => return Err(LoginErr::ServerDownErr), - StatusCode::INTERNAL_SERVER_ERROR => return Err(LoginErr::ServerDown500Err), - _ => {} - } - - let mut refresh_header = resp.headers().get("refresh").map(|v| v.to_str().unwrap()).unwrap_or(""); - while refresh_header != "" { - let rgx = Regex::new(r#"URL=(.+)"#).unwrap(); - let refresh_url = format!("{}{}", &self.config.url, rgx.captures(&refresh_header).unwrap().get(1).unwrap().as_str()); - println!("waitroom enabled, wait 10sec"); - thread::sleep(Duration::from_secs(10)); - resp = self.client.get(refresh_url.clone()).send()?; - refresh_header = resp.headers().get("refresh").map(|v| v.to_str().unwrap()).unwrap_or(""); - } - - let mut resp = resp.text()?; - if resp.contains(CAPTCHA_USED_ERR) { - return Err(LoginErr::CaptchaUsedErr); - } else if resp.contains(CAPTCHA_WG_ERR) { - return Err(LoginErr::CaptchaWgErr); - } else if resp.contains(REG_ERR) { - return Err(LoginErr::RegErr); - } else if resp.contains(NICKNAME_ERR) { - return Err(LoginErr::NicknameErr); - } else if resp.contains(KICKED_ERR) { - return Err(LoginErr::KickedErr); } - - let mut doc = Document::from(resp.as_str()); - if let Some(body) = doc.find(Name("body")).next() { - if let Some(body_class) = body.attr("class") { - if body_class == "error" { - if let Some(h2) = doc.find(Name("h2")).next() { - log::error!("{}", h2.text()); - } - return Err(LoginErr::UnknownErr); - } else if body_class == "failednotice" { - log::error!("failed logins: {}", body.text()); - let nc = doc.find(Attr("name", "nc")).next().unwrap(); - let nc_value = nc.attr("value").unwrap().to_owned(); - let params: Vec<(&str, String)> = vec![ - ("lang", LANG.to_owned()), - ("nc", nc_value.to_owned()), - ("action", "login".to_owned()), - ]; - resp = self.client.post(&login_url).form(&params).send()?.text()?; - doc = Document::from(resp.as_str()); - } - } - } - - let iframe = match doc.find(Attr("name", "view")).next() { - Some(view) => view, - None => { - fs::write("./dump_login_err.html", resp.as_str()).unwrap(); - panic!("failed to get view iframe"); - } - }; - let iframe_src = iframe.attr("src").unwrap(); - - let session_captures = SESSION_RGX.captures(iframe_src).unwrap(); - let session = session_captures.get(1).unwrap().as_str(); - - self.session = session.to_owned(); + self.session = lechatphp::login( + &self.client, &self.config.url, &self.config.page_php, &self.base_client.username, + &self.base_client.password, &self.guest_color, self.manual_captcha)?; Ok(()) } - fn logout(&mut self) -> Result<()> { - let full_url = format!("{}/{}", &self.config.url, &self.config.page_php); - let params = [ - ("action", "logout"), - ("session", &self.session), - ("lang", LANG), - ]; - self.client.post(&full_url).form(&params).send()?; + fn logout(&mut self) -> anyhow::Result<()> { + lechatphp::logout(&self.client, &self.config.url, &self.config.page_php, &self.session)?; self.session = "".to_owned(); Ok(()) } @@ -978,7 +813,7 @@ impl LeChatPHPClient { }); } - fn handle_input(&mut self, events: &Events, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>) -> std::result::Result<(), ExitSignal> { + fn handle_input(&mut self, events: &Events, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>) -> Result<(), ExitSignal> { match events.next() { Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin), Ok(Event::Terminate) => return Err(ExitSignal::Terminate), @@ -987,7 +822,7 @@ impl LeChatPHPClient { } } - fn handle_event(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>, event: event::Event) -> std::result::Result<(), ExitSignal> { + fn handle_event(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>, event: event::Event) -> Result<(), ExitSignal> { match event { event::Event::Resize(_cols, _rows) => Ok(()), event::Event::FocusGained => Ok(()), @@ -998,7 +833,7 @@ impl LeChatPHPClient { } } - fn handle_key_event(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>, key_event: KeyEvent) -> std::result::Result<(), ExitSignal> { + fn handle_key_event(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>, key_event: KeyEvent) -> Result<(), ExitSignal> { match app.input_mode { InputMode::LongMessage => self.handle_long_message_mode_key_event(app, key_event, messages), InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages), @@ -1007,7 +842,7 @@ impl LeChatPHPClient { } } - fn handle_long_message_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, messages: &Arc<Mutex<Vec<Message>>>) -> std::result::Result<(), ExitSignal> { + fn handle_long_message_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, messages: &Arc<Mutex<Vec<Message>>>) -> Result<(), ExitSignal> { match key_event { KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } | KeyEvent { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, .. } => self.handle_long_message_mode_key_event_esc(app), @@ -1017,7 +852,7 @@ impl LeChatPHPClient { Ok(()) } - fn handle_normal_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, messages: &Arc<Mutex<Vec<Message>>>) -> std::result::Result<(), ExitSignal> { + fn handle_normal_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, messages: &Arc<Mutex<Vec<Message>>>) -> Result<(), ExitSignal> { match key_event { KeyEvent { code: KeyCode::Char('/'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_slash(app), KeyEvent { code: KeyCode::Char('j'), modifiers: KeyModifiers::NONE, .. } | @@ -1052,7 +887,7 @@ impl LeChatPHPClient { Ok(()) } - fn handle_editing_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, users: &Arc<Mutex<Users>>) -> std::result::Result<(), ExitSignal> { + fn handle_editing_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, users: &Arc<Mutex<Users>>) -> Result<(), ExitSignal> { app.input_mode = InputMode::Editing; match key_event { KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_enter(app)?, @@ -1230,12 +1065,12 @@ impl LeChatPHPClient { app.items.unselect(); } - fn handle_normal_mode_key_event_logout(&mut self) -> std::result::Result<(), ExitSignal> { + fn handle_normal_mode_key_event_logout(&mut self) -> Result<(), ExitSignal> { self.logout().unwrap(); return Err(ExitSignal::Terminate); } - fn handle_normal_mode_key_event_exit(&mut self) -> std::result::Result<(), ExitSignal> { + fn handle_normal_mode_key_event_exit(&mut self) -> Result<(), ExitSignal> { return Err(ExitSignal::Terminate); } @@ -1316,7 +1151,7 @@ impl LeChatPHPClient { app.items.state.select(Some(0)); } - fn handle_editing_mode_key_event_enter(&mut self, app: &mut App) -> std::result::Result<(), ExitSignal> { + fn handle_editing_mode_key_event_enter(&mut self, app: &mut App) -> Result<(), ExitSignal> { if FIND_RGX.is_match(&app.input) { return Ok(()); } @@ -1562,7 +1397,7 @@ impl LeChatPHPClient { &mut self, app: &mut App, mouse_event: MouseEvent, - ) -> std::result::Result<(), ExitSignal> { + ) -> Result<(), ExitSignal> { match mouse_event.kind { MouseEventKind::ScrollDown => app.items.next(), MouseEventKind::ScrollUp => app.items.previous(), @@ -1620,7 +1455,7 @@ fn set_profile_base_info( client: &Client, full_url: &str, params: &mut Vec<(&str, String)>, -) -> Result<()> { +) -> anyhow::Result<()> { params.extend(vec![("action", "profile".to_owned())]); let profile_resp = client.post(full_url).form(&params).send()?; let profile_resp_txt = profile_resp.text().unwrap(); @@ -1654,7 +1489,7 @@ fn delete_message( params: &mut Vec<(&str, String)>, date: String, text: String, -) -> Result<()> { +) -> anyhow::Result<()> { params.extend(vec![ ("action", "admin".to_owned()), ("do", "clean".to_owned()), @@ -1769,6 +1604,7 @@ fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient { let (color_tx, color_rx) = crossbeam_channel::unbounded(); let (tx, rx) = crossbeam_channel::unbounded(); LeChatPHPClient { + chat_type: params.chat_type, base_client: BaseClient { username: params.username, password: params.password, @@ -1890,6 +1726,7 @@ fn get_guest_color(wanted: Option<String>) -> String { fn get_tor_client(socks_proxy_url: &str, no_proxy: bool) -> Client { let ua = "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0"; let mut builder = reqwest::blocking::ClientBuilder::new() + .redirect(Policy::none()) .cookie_store(true) .user_agent(ua); if !no_proxy { @@ -1916,7 +1753,7 @@ fn ask_password(password: Option<String>) -> String { }) } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] enum ClientType { BHC, Dan, @@ -2005,7 +1842,7 @@ fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) { }); } -fn main() -> Result<()> { +fn main() -> anyhow::Result<()> { let mut opts: Opts = Opts::parse(); // Configs file @@ -2375,11 +2212,11 @@ fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str { } } -fn extract_messages(doc: &Document) -> Result<Vec<Message>> { +fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> { let msgs = doc .find(Attr("id", "messages")) .next() - .ok_or("failed to get messages div")? + .ok_or(anyhow!("failed to get messages div"))? .find(Attr("class", "msg")) .filter_map(|tag| { let mut id: Option<usize> = None; @@ -2866,7 +2703,7 @@ impl Events { ) } - fn next(&self) -> std::result::Result<Event<CEvent>, crossbeam_channel::RecvError> { + fn next(&self) -> Result<Event<CEvent>, crossbeam_channel::RecvError> { select! { recv(&self.rx) -> evt => evt, recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick),