commit 51a2516528baa2d7315e25f3a4f6e54f4bd5ad9c
parent 6a8e8bfe731b86fffdd37caa05eb00a80aebc4e1
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Wed, 29 Mar 2023 17:07:34 -0700
local captcha solver
Diffstat:
| M | src/lechatphp/mod.rs | | | 62 | ++++++++++++++++++++++++++++---------------------------------- |
| M | src/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(¶ms)
- .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 {