bhcli

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

commit 51a2516528baa2d7315e25f3a4f6e54f4bd5ad9c
parent 6a8e8bfe731b86fffdd37caa05eb00a80aebc4e1
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Wed, 29 Mar 2023 17:07:34 -0700

local captcha solver

Diffstat:
Msrc/lechatphp/mod.rs | 62++++++++++++++++++++++++++++----------------------------------
Msrc/main.rs | 77++++++++++++++++++++++++++++++-----------------------------------------------
2 files changed, 58 insertions(+), 81 deletions(-)

diff --git a/src/lechatphp/mod.rs b/src/lechatphp/mod.rs @@ -89,7 +89,7 @@ pub fn solve_b64(b64_str: &str) -> Option<String> { let img_dec = general_purpose::STANDARD.decode(b64_str.strip_prefix("data:image/gif;base64,")?).ok()?; let img = image::load_from_memory(&img_dec).ok()?; if img.dimensions().0 > 60 { - return Some(solve_difficulty3(&img).unwrap()); + return solve_difficulty3(&img).ok(); } solve_difficulty2(&img) } @@ -212,7 +212,6 @@ fn solve_difficulty3(img: &DynamicImage) -> Result<String, String> { } let letter = Letter{offset_x: x, offset_y: y, character: c}; - println!("Letter: {:?}", &letter); letters_map.insert(c, letter); // Keep letters in hashmap for easy access break; } @@ -228,7 +227,6 @@ fn solve_difficulty3(img: &DynamicImage) -> Result<String, String> { for (_, letter) in letters_map.iter() { let square = img.crop_imm(letter.offset_x-5, letter.offset_y-3, 8+5+6, 14+3+2); let count_red = count_red_px(&square); - // println!("{}, {}", letter.character, count_red); if count_red > min_starting_pt_red_px { starting = Some(letter.clone()); break; @@ -239,8 +237,6 @@ fn solve_difficulty3(img: &DynamicImage) -> Result<String, String> { return Err("could not find starting letter".to_owned()); } - // println!("STARTING IS {:?}", starting.clone().unwrap()); - let mut answer = String::new(); let mut letter = starting.unwrap(); let mut visited = HashSet::<String>::new(); @@ -256,68 +252,49 @@ fn solve_difficulty3(img: &DynamicImage) -> Result<String, String> { } let top_left = Point::new(letter.offset_x-5, letter.offset_y-3); - let mut rect_min = Point::new(top_left.x, top_left.y); - let mut rect_dim = (8+5+6, 14+3+2); + let mut rect = Rect{p1: top_left, width: 8+5+6, height: 14+3+2}; let mut retry = 0; 'the_loop: loop { retry += 1; - let square = img.crop_imm(rect_min.x, rect_min.y, rect_dim.0, rect_dim.1); - if i == 0 { - square.save("root.gif").unwrap(); - } - let red_px_pts = get_contour_red_pixels(&rect_min, &square); + let square = img.crop_imm(rect.p1.x, rect.p1.y, rect.width, rect.height); + let red_px_pts = get_contour_red_pixels(&rect.p1, &square); if i == 0 { if red_px_pts.len() == 0 { if retry < 10 { - rect_min.x -= 1; - rect_min.y -= 1; - rect_dim.0 += 1; - rect_dim.1 += 1; + rect.enlarge(); continue; } - square.save(format!("captcha_root_has_no_line.gif")).unwrap(); return Err(format!("root {:?} has no line detected", letter)); } if red_px_pts.len() > 1 { - square.save(format!("captcha_root_more_than_one_line.gif")).unwrap(); return Err(format!("root {:?} has more than one line detected", letter)); } let red_pt = red_px_pts.get(0).unwrap(); let angle = get_angle(red_pt, &letter.center()); let neighbor = get_letter_in_direction(&letter, angle, &letters_map); letter = neighbor.clone(); - println!("angle: {:?}, neighbor: {:?}", angle, neighbor.clone()); break; } if red_px_pts.len() == 0 { if retry < 10 { - rect_min.x -= 1; - rect_min.y -= 1; - rect_dim.0 += 1; - rect_dim.1 += 1; + rect.enlarge(); continue; } - square.save(format!("captcha_has_no_line.gif")).unwrap(); return Err(format!("letter #{} {:?} has no line detected", i+1, letter)); } if red_px_pts.len() == 1 { if retry < 10 { - rect_min.x -= 1; - rect_min.y -= 1; - rect_dim.0 += 1; - rect_dim.1 += 1; + rect.enlarge(); continue; } - square.save(format!("captcha_has_only_one_line.gif")).unwrap(); return Err(format!("letter #{} {:?} has only 1 line detected", i+1, letter)); } if red_px_pts.len() > 2 { - square.save(format!("captcha_has_more_than_two_lines.gif")).unwrap(); return Err(format!("letter #{} {:?} has more than 2 lines detected", i+1, letter)); } @@ -380,6 +357,22 @@ fn get_angle(p1: &Point, p2: &Point) -> f64 { (p1.y as f64 - p2.y as f64).atan2(p1.x as f64 - p2.x as f64) } +#[derive(Debug)] +struct Rect { + p1: Point, + width: u32, + height: u32, +} + +impl Rect { + fn enlarge(&mut self) { + self.p1.x = std::cmp::max(self.p1.x-1, 0); + self.p1.y = std::cmp::max(self.p1.y-1, 0); + self.width += 1; + self.height += 1; + } +} + #[derive(Debug, Clone)] struct Point { x: u32, @@ -441,7 +434,7 @@ fn get_contour_red_pixels(top_left_pt: &Point, img: &DynamicImage) -> Vec<Point> fn get_pixel_in_bound(img: &DynamicImage, x: u32, y: u32) -> Option<Rgba<u8>> { let dim = img.dimensions(); - if x > dim.0 || y > dim.1 { + if x >= dim.0 || y >= dim.1 { return None; } Some(img.get_pixel(x, y)) @@ -471,9 +464,10 @@ fn has_red_in_center_area(letter_img: &DynamicImage) -> bool { let red_color = Rgba::from([204, 2, 4, 255]); for y in 5..=std::cmp::min(letter_img.dimensions().1, 7) { for x in 3..=std::cmp::min(letter_img.dimensions().0, 5) { - let letter_img_color = letter_img.get_pixel(x, y); - if letter_img_color == red_color { - return true; + if let Some(letter_img_color) = get_pixel_in_bound(letter_img, x, y) { + if letter_img_color == red_color { + return true; + } } } } diff --git a/src/main.rs b/src/main.rs @@ -70,6 +70,7 @@ const KICKED_ERR: &str = "You have been kicked"; const REG_ERR: &str = "This nickname is a registered member"; const NICKNAME_ERR: &str = "Invalid nickname"; 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 N0TR1V: &str = "n0tr1v"; @@ -251,6 +252,9 @@ impl LeChatPHPClient { { eprintln!("{:?}", e.to_string()); break; + } else if e.to_string() == CAPTCHA_FAILED_SOLVE_ERR { + eprintln!("{}", e.to_string()); + continue; } else if e.to_string() == CAPTCHA_WG_ERR || e.to_string() == CAPTCHA_USED_ERR { } else if e.to_string() == SERVER_DOWN_ERR || e.to_string() == SERVER_DOWN_500_ERR { eprintln!("{}", e.to_string()); @@ -806,54 +810,35 @@ impl LeChatPHPClient { { let captcha_value = captcha_value.attr("value").unwrap(); + let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); + let mut captcha_input = String::new(); if self.manual_captcha { - let captcha_img = doc.find(Name("img")).next().unwrap().attr("src").unwrap(); - if let Some(answer) = lechatphp::solve_b64(captcha_img) { - captcha_input = answer; - } else if let Some(dkf_api_key) = self.dkf_api_key.clone() { - // If we have the DKF_API_KEY, auto solve captcha using the api - let params = vec![("captcha", captcha_img)]; - let resp = self - .client - .post(format!("{}/api/v1/captcha/solver", DKF_URL)) - .header("DKF_API_KEY", dkf_api_key) - .form(&params) - .send()?; - if resp.status() != StatusCode::OK { - return Err(CAPTCHA_WG_ERR.into()); - } - let resp = resp.text()?; - let rgx = Regex::new(r#""answer": "([^"]+)""#)? - .captures(resp.as_str()) - .unwrap(); - let answer = rgx.get(1).unwrap().as_str(); - captcha_input = answer.to_owned(); - } else { - // 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())?; - 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); - } + // 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())?; + 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 { - // Captcha is not actually required for memebers (BHC) - captcha_input = "12345".to_owned(); + match lechatphp::solve_b64(captcha_img) { + Some(answer) => captcha_input = answer, + None => return Err(CAPTCHA_FAILED_SOLVE_ERR.into()), + } } params.extend(vec![ @@ -1696,11 +1681,9 @@ impl ChatClient { }, ClientType::Dan => { c.config = LeChatPHPConfig::new_dans_chat_config(); - c.manual_captcha = true; }, ClientType::BHC => { c.config = LeChatPHPConfig::new_black_hat_chat_config(); - c.manual_captcha = true; } } Self {