captchaforge 0.2.7

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
//! Generic slider / swipe / rotate / drag CAPTCHA detector.
//!
//! Coverage is intentionally broad to catch the long tail of in-house
//! slider widgets ("class contains 'slider', 'swipe', 'rotate',
//! 'motion'"). Specific vendors with stable signatures (Geetest v3/v4
//! etc.) should ship their own dedicated detector at a lower priority
//! value so they win over this generic one.
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;

use super::{parse_detection_result, CaptchaInfo, Detector};

/// Detector for generic slider-style CAPTCHAs.
pub struct SliderCaptchaDetector;

impl SliderCaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const slider = document.querySelector('.slider-captcha, #slider, [class*="slider"], [class*="swipe"], [class*="rotate"], [class*="motion"]');
    if (slider) {
        return { kind: 'slider', site_key: null, container: '.slider-captcha, #slider, [class*="slider"], [class*="swipe"], [class*="rotate"], [class*="motion"]' };
    }
    const rangeInCaptcha = document.querySelector('.captcha input[type="range"], #captcha input[type="range"], [class*="captcha"] input[type="range"]');
    if (rangeInCaptcha) {
        return { kind: 'slider', site_key: null, container: '.captcha input[type="range"], #captcha input[type="range"], [class*="captcha"] input[type="range"]' };
    }
    const canvasDrag = document.querySelector('canvas[class*="drag"], canvas[class*="rotate"], canvas[class*="slider"], canvas[class*="swipe"]');
    if (canvasDrag) {
        return { kind: 'slider', site_key: null, container: 'canvas[class*="drag"], canvas[class*="rotate"], canvas[class*="slider"], canvas[class*="swipe"]' };
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for SliderCaptchaDetector {
    fn name(&self) -> &'static str {
        "slider_captcha"
    }
    fn priority(&self) -> i32 {
        75
    }
    async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
        let raw = page.evaluate(Self::JS).await?;
        let val = raw.into_value::<serde_json::Value>()?;
        Ok(parse_detection_result(val))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn slider_captcha_detector_js_has_selectors() {
        assert!(SliderCaptchaDetector::JS.contains(".slider-captcha"));
        assert!(SliderCaptchaDetector::JS.contains("#slider"));
        assert!(SliderCaptchaDetector::JS.contains("[class*=\"slider\"]"));
        assert!(SliderCaptchaDetector::JS.contains("[class*=\"swipe\"]"));
        assert!(SliderCaptchaDetector::JS.contains("[class*=\"rotate\"]"));
        assert!(SliderCaptchaDetector::JS.contains("[class*=\"motion\"]"));
        assert!(SliderCaptchaDetector::JS.contains("input[type=\"range\"]"));
        assert!(SliderCaptchaDetector::JS.contains("canvas[class*=\"drag\"]"));
    }
}