use crate::behavior::{self, ScrollDirection};
use crate::Page;
use anyhow::{anyhow, Result};
use rand::{Rng, SeedableRng};
use std::time::{Duration, Instant};
pub const DEFAULT_WARMUP_MS: u64 = 4_000;
pub async fn apply_default_warmup(page: &Page) -> Result<()> {
if let Err(e) = warm_session(page, Duration::from_millis(DEFAULT_WARMUP_MS)).await {
tracing::warn!(error = %e, "captchaforge: pre-solve warmup failed (continuing); behavioral cover is weaker for the first solve attempt");
}
Ok(())
}
pub async fn warm_session(page: &Page, budget: Duration) -> Result<()> {
let start = Instant::now();
let deadline = start + budget;
let viewport = page
.evaluate("({w: window.innerWidth || 1280, h: window.innerHeight || 800})")
.await
.map_err(|e| anyhow!("warmup viewport probe failed: {e}"))?
.into_value::<serde_json::Value>()
.unwrap_or(serde_json::Value::Null);
let vw = viewport["w"].as_f64().unwrap_or(1280.0);
let vh = viewport["h"].as_f64().unwrap_or(800.0);
let mut rng = rand::rngs::StdRng::from_entropy();
let mut cx = vw * 0.5 + rng.gen_range(-50.0..50.0);
let mut cy = vh * 0.5 + rng.gen_range(-50.0..50.0);
while Instant::now() < deadline {
let action = rng.gen_range(0..10);
let result: Result<()> = match action {
0..=4 => {
let tx = rng.gen_range(50.0..(vw - 50.0).max(50.0));
let ty = rng.gen_range(50.0..(vh - 50.0).max(50.0));
let r = behavior::mouse_move_human(page, cx, cy, tx, ty).await;
cx = tx;
cy = ty;
r
}
5..=6 => {
let down = rng.gen_range(80..240);
let up = rng.gen_range((down / 2)..=down);
match behavior::scroll_realistic(page, ScrollDirection::Down, down).await {
Ok(()) => {
tokio::time::sleep(Duration::from_millis(rng.gen_range(180..500))).await;
behavior::scroll_realistic(page, ScrollDirection::Up, up).await
}
Err(e) => Err(e),
}
}
7 => {
let hover_js = r#"
(() => {
const els = document.querySelectorAll('a, button, h1, h2, h3, p');
if (els.length === 0) return null;
const el = els[Math.floor(Math.random() * els.length)];
const r = el.getBoundingClientRect();
return [r.left + r.width / 2, r.top + r.height / 2];
})()
"#;
if let Ok(raw) = page.evaluate(hover_js).await {
if let Ok(Some([tx, ty])) = raw.into_value::<Option<[f64; 2]>>() {
if tx.is_finite() && ty.is_finite() && tx > 0.0 && ty > 0.0 {
let r = behavior::mouse_move_human(page, cx, cy, tx, ty).await;
cx = tx;
cy = ty;
tokio::time::sleep(Duration::from_millis(rng.gen_range(200..600)))
.await;
r
} else {
Ok(())
}
} else {
Ok(())
}
} else {
Ok(())
}
}
_ => {
tokio::time::sleep(Duration::from_millis(rng.gen_range(120..400))).await;
Ok(())
}
};
if let Err(e) = result {
tracing::warn!(error = %e, "captchaforge: warmup action failed (continuing); behavioral cover is weaker for this solve");
}
tokio::time::sleep(Duration::from_millis(rng.gen_range(80..250))).await;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_warmup_ms_is_4_seconds() {
assert_eq!(DEFAULT_WARMUP_MS, 4_000);
}
#[test]
fn module_exports_warm_session() {
let _: &str = std::any::type_name_of_val(&warm_session);
}
}