captchaforge 0.2.39

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! Pure text utilities for the reCAPTCHA audio solver: normalise an STT
//! transcript for the answer field, recognise the rate-limit interstitial, and
//! validate an audio-clip URL. Re-exported at `captchaforge::solver::*` (the
//! doctests pin that public path), so moving them here is path-transparent.

use super::RATE_LIMIT_PHRASES;

/// Normalise an STT transcript for typing into the audio-response field.
///
/// reCAPTCHA's audio answers are space-separated words/digits; STT
/// engines love to add capitalisation and trailing punctuation. The
/// response field is matched case-insensitively but stray punctuation
/// counts as wrong characters.
///
/// # Examples
///
/// ```
/// use captchaforge::solver::clean_transcript;
/// assert_eq!(clean_transcript("  Hello, World.  "), "hello world");
/// assert_eq!(clean_transcript("five seven, nine!"), "five seven nine");
/// assert_eq!(clean_transcript("\nthree\tfour\n"), "three four");
/// assert_eq!(clean_transcript(""), "");
/// ```
pub fn clean_transcript(raw: &str) -> String {
    let lowered = raw.to_lowercase();
    let mut buf = String::with_capacity(lowered.len());
    let mut last_space = true;
    for ch in lowered.chars() {
        if ch.is_alphanumeric() {
            buf.push(ch);
            last_space = false;
        } else if ch.is_whitespace() || matches!(ch, ',' | '.' | '!' | '?' | ';' | ':') {
            if !last_space {
                buf.push(' ');
                last_space = true;
            }
        } else {
            // Ignore other punctuation entirely: STT noise.
        }
    }
    buf.trim().to_string()
}

/// True when `text` matches a known reCAPTCHA rate-limit interstitial.
///
/// # Examples
///
/// ```
/// use captchaforge::solver::is_rate_limited;
/// assert!(is_rate_limited(
///     "Your computer or network may be sending automated queries"
/// ));
/// assert!(is_rate_limited("please TRY AGAIN LATER"));
/// assert!(!is_rate_limited("Press PLAY to listen"));
/// ```
pub fn is_rate_limited(text: &str) -> bool {
    let lower = text.to_lowercase();
    RATE_LIMIT_PHRASES
        .iter()
        .any(|p| lower.contains(&p.to_lowercase()))
}

/// True when `s` is a plausible reCAPTCHA audio MP3 URL. Non-empty,
/// http(s), and either the recaptcha host or a `.mp3` suffix.
///
/// # Examples
///
/// ```
/// use captchaforge::solver::looks_like_audio_url;
/// assert!(looks_like_audio_url(
///     "https://www.google.com/recaptcha/api2/payload?p=clip.mp3"
/// ));
/// assert!(looks_like_audio_url("http://cdn.example.com/x.mp3"));
/// assert!(!looks_like_audio_url(""));
/// assert!(!looks_like_audio_url("data:audio/mpeg;base64,abc"));
/// assert!(!looks_like_audio_url("blob:https://x"));
/// ```
pub fn looks_like_audio_url(s: &str) -> bool {
    if !(s.starts_with("https://") || s.starts_with("http://")) {
        return false;
    }
    s.contains("google.com/recaptcha") || s.to_lowercase().ends_with(".mp3")
}