use chromiumoxide::cdp::browser_protocol::input::{
DispatchKeyEventParams, DispatchKeyEventType, DispatchMouseEventParams, DispatchMouseEventType,
MouseButton,
};
use chromiumoxide::Page;
use rand::{Rng, SeedableRng};
use std::time::Duration;
pub async fn mouse_move_bezier(
page: &Page,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
) -> anyhow::Result<()> {
let mut rng = rand::rngs::StdRng::from_entropy();
let steps = rng.gen_range(8..=20);
let cx1 = x0 + (x1 - x0) * rng.gen_range(0.2..0.5) + rng.gen_range(-30.0..30.0);
let cy1 = y0 + (y1 - y0) * rng.gen_range(0.1..0.4) + rng.gen_range(-20.0..20.0);
let cx2 = x0 + (x1 - x0) * rng.gen_range(0.6..0.9) + rng.gen_range(-20.0..20.0);
let cy2 = y0 + (y1 - y0) * rng.gen_range(0.5..0.8) + rng.gen_range(-15.0..15.0);
for i in 0..=steps {
let t = i as f64 / steps as f64;
let u = 1.0 - t;
let x = u * u * u * x0 + 3.0 * u * u * t * cx1 + 3.0 * u * t * t * cx2 + t * t * t * x1;
let y = u * u * u * y0 + 3.0 * u * u * t * cy1 + 3.0 * u * t * t * cy2 + t * t * t * y1;
let params = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseMoved)
.x(x)
.y(y)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(params).await?;
let ease = (std::f64::consts::PI * t).sin(); let base_ms = rng.gen_range(8..25);
let delay_ms = base_ms + ((1.0 - ease) * 15.0) as u64;
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
Ok(())
}
pub async fn mouse_move_human(
page: &Page,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
) -> anyhow::Result<()> {
let mut rng = rand::rngs::StdRng::from_entropy();
let trace = crate::mouse_traces::pick_trace(&mut rng);
crate::mouse_traces::replay_trace(page, x0, y0, x1, y1, trace, 1.5).await
}
pub async fn click_realistic(page: &Page, x: f64, y: f64) -> anyhow::Result<()> {
let mut rng = rand::rngs::StdRng::from_entropy();
let down = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MousePressed)
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(1)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(down).await?;
tokio::time::sleep(Duration::from_millis(rng.gen_range(50..150))).await;
let up = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseReleased)
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(1)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(up).await?;
Ok(())
}
pub async fn type_realistic(page: &Page, text: &str) -> anyhow::Result<()> {
let mut rng = rand::rngs::StdRng::from_entropy();
for (i, ch) in text.chars().enumerate() {
let key_text = ch.to_string();
let down = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyDown)
.text(&key_text)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(down).await?;
tokio::time::sleep(Duration::from_millis(rng.gen_range(30..80))).await;
let up = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyUp)
.text(&key_text)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(up).await?;
let base = rng.gen_range(80..250);
let pause = if i > 0 && i % rng.gen_range(4..10) == 0 {
rng.gen_range(200..600) } else {
0
};
tokio::time::sleep(Duration::from_millis(base + pause)).await;
}
Ok(())
}
pub async fn type_human(page: &Page, text: &str) -> anyhow::Result<()> {
use crate::keystroke_timing::{plan_keystrokes, TypingPlan};
let mut rng = rand::rngs::StdRng::from_entropy();
let plan = plan_keystrokes(text, TypingPlan::default(), &mut rng);
for k in plan {
if k.gap_ms_before > 0 {
tokio::time::sleep(Duration::from_millis(k.gap_ms_before as u64)).await;
}
let down_builder = if k.ch == '\u{0008}' {
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyDown)
.key("Backspace")
.windows_virtual_key_code(8)
} else {
let key_text = k.ch.to_string();
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyDown)
.text(key_text)
};
page.execute(down_builder.build().map_err(anyhow::Error::msg)?)
.await?;
tokio::time::sleep(Duration::from_millis(k.hold_ms as u64)).await;
let up_builder = if k.ch == '\u{0008}' {
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyUp)
.key("Backspace")
.windows_virtual_key_code(8)
} else {
let key_text = k.ch.to_string();
DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyUp)
.text(key_text)
};
page.execute(up_builder.build().map_err(anyhow::Error::msg)?)
.await?;
}
Ok(())
}
pub async fn scroll_realistic(
page: &Page,
direction: ScrollDirection,
amount: u32,
) -> anyhow::Result<()> {
let mut rng = rand::rngs::StdRng::from_entropy();
let steps = rng.gen_range(3..=8).min(amount);
let per_step = (amount as f64 / steps as f64) as i32;
for _ in 0..steps {
let delta = match direction {
ScrollDirection::Down => per_step + rng.gen_range(-20..20),
ScrollDirection::Up => -(per_step + rng.gen_range(-20..20)),
};
let js = format!("window.scrollBy(0, {})", delta);
page.evaluate(js).await?;
tokio::time::sleep(Duration::from_millis(rng.gen_range(100..400))).await;
}
Ok(())
}
pub async fn idle_pause() {
let ms = rand::rngs::StdRng::from_entropy().gen_range(500..2000);
tokio::time::sleep(Duration::from_millis(ms)).await;
}
pub async fn micro_pause() {
let ms = rand::rngs::StdRng::from_entropy().gen_range(100..400);
tokio::time::sleep(Duration::from_millis(ms)).await;
}
pub async fn random_pause(min_ms: u64, max_ms: u64) {
assert!(
min_ms <= max_ms,
"random_pause: min_ms ({min_ms}) > max_ms ({max_ms})"
);
if min_ms == max_ms {
tokio::time::sleep(Duration::from_millis(min_ms)).await;
return;
}
let ms = rand::rngs::StdRng::from_entropy().gen_range(min_ms..=max_ms);
tokio::time::sleep(Duration::from_millis(ms)).await;
}
pub async fn triple_click(page: &Page, x: f64, y: f64) -> anyhow::Result<()> {
use chromiumoxide::cdp::browser_protocol::input::DispatchMouseEventParams;
for click_count in 1i64..=3 {
let down = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MousePressed)
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(click_count)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(down).await?;
let up = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseReleased)
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(click_count)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(up).await?;
tokio::time::sleep(Duration::from_millis(60)).await;
}
Ok(())
}
pub async fn hover_dwell(page: &Page, x: f64, y: f64) -> anyhow::Result<()> {
use chromiumoxide::cdp::browser_protocol::input::DispatchMouseEventParams;
let mv = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseMoved)
.x(x)
.y(y)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(mv).await?;
let ms = rand::rngs::StdRng::from_entropy().gen_range(200..700);
tokio::time::sleep(Duration::from_millis(ms)).await;
Ok(())
}
pub async fn touch_swipe(
page: &Page,
x0: f64,
y0: f64,
x1: f64,
y1: f64,
duration_ms: u64,
) -> anyhow::Result<()> {
use chromiumoxide::cdp::browser_protocol::input::DispatchMouseEventParams;
let down = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MousePressed)
.x(x0)
.y(y0)
.button(MouseButton::Left)
.click_count(1)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(down).await?;
let steps = 25;
let step_ms = (duration_ms / steps as u64).max(8);
for s in 1..=steps {
let t = s as f64 / steps as f64;
let eased = 1.0 - (1.0 - t).powi(2);
let x = x0 + (x1 - x0) * eased;
let y = y0 + (y1 - y0) * eased;
let mv = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseMoved)
.x(x)
.y(y)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(mv).await?;
tokio::time::sleep(Duration::from_millis(step_ms)).await;
}
let up = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseReleased)
.x(x1)
.y(y1)
.button(MouseButton::Left)
.click_count(1)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(up).await?;
Ok(())
}
#[cfg(test)]
mod random_pause_tests {
use super::*;
use std::time::Instant;
#[tokio::test]
async fn random_pause_returns_within_window() {
let start = Instant::now();
random_pause(50, 80).await;
let elapsed_ms = start.elapsed().as_millis() as u64;
assert!(
(50..=130).contains(&elapsed_ms),
"random_pause(50, 80) elapsed {elapsed_ms}ms (expected 50..=130)"
);
}
#[tokio::test]
async fn random_pause_with_equal_bounds_does_not_panic() {
let start = Instant::now();
random_pause(40, 40).await;
let elapsed_ms = start.elapsed().as_millis() as u64;
assert!(elapsed_ms >= 40);
}
#[tokio::test]
#[should_panic(expected = "random_pause: min_ms")]
async fn random_pause_panics_when_min_exceeds_max() {
random_pause(200, 100).await;
}
}
pub async fn pointer_jitter(
page: &Page,
cx: f64,
cy: f64,
cycles: u32,
cycle_ms: u64,
) -> anyhow::Result<()> {
use chromiumoxide::cdp::browser_protocol::input::DispatchMouseEventParams;
let mut rng = rand::rngs::StdRng::from_entropy();
for c in 0..cycles {
let amp_x: f64 = rng.gen_range(2.0..4.5);
let amp_y: f64 = rng.gen_range(2.0..4.5);
let phase: f64 = rng.gen_range(0.0..std::f64::consts::TAU);
let steps = 8u64;
for s in 0..steps {
let theta = phase + (c as f64 + s as f64 / steps as f64) * std::f64::consts::TAU;
let x = cx + amp_x * theta.sin();
let y = cy + amp_y * (theta * 1.7).cos();
let mv = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseMoved)
.x(x)
.y(y)
.build()
.map_err(anyhow::Error::msg)?;
page.execute(mv).await?;
tokio::time::sleep(Duration::from_millis(cycle_ms / steps)).await;
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollDirection {
Up,
Down,
}
#[cfg(test)]
mod tests {
#[test]
fn bezier_control_points_within_range() {
let (x0, _y0) = (100.0, 200.0);
let (x1, _y1) = (500.0, 400.0);
let t0 = 0.0_f64;
let t1 = 1.0_f64;
let u0 = 1.0 - t0;
let u1 = 1.0 - t1;
let bx0 = u0.powi(3) * x0 + t0.powi(3) * x1;
let bx1 = u1.powi(3) * x0 + t1.powi(3) * x1;
assert!((bx0 - x0).abs() < 0.001);
assert!((bx1 - x1).abs() < 0.001);
}
use super::*;
#[test]
fn scroll_direction_clone_copy() {
let d = ScrollDirection::Down;
let d2 = d;
assert_eq!(d, d2);
}
#[test]
fn scroll_direction_debug() {
let d = ScrollDirection::Up;
assert!(format!("{:?}", d).contains("Up"));
}
#[test]
fn bezier_midpoint_t_0_5() {
let x0 = 0.0;
let y0 = 0.0;
let x1 = 100.0;
let y1 = 0.0;
let cx1 = 25.0;
let cy1 = 50.0;
let cx2 = 75.0;
let cy2 = 50.0;
let t = 0.5;
let u = 1.0 - t;
let x = u * u * u * x0 + 3.0 * u * u * t * cx1 + 3.0 * u * t * t * cx2 + t * t * t * x1;
let y = u * u * u * y0 + 3.0 * u * u * t * cy1 + 3.0 * u * t * t * cy2 + t * t * t * y1;
assert!((x - 50.0_f64).abs() < 1.0);
assert!((y - 37.5_f64).abs() < 1.0);
}
#[test]
fn bezier_is_linear_when_control_points_on_line() {
let x0 = 0.0;
let x1 = 100.0;
let cx1 = 33.0;
let cx2 = 66.0;
for i in 0..=10 {
let t = i as f64 / 10.0;
let u = 1.0 - t;
let x = u * u * u * x0 + 3.0 * u * u * t * cx1 + 3.0 * u * t * t * cx2 + t * t * t * x1;
assert!((x - (t * 100.0)).abs() < 1.0, "t={} x={}", t, x);
}
}
#[test]
fn ease_function_zero_at_endpoints() {
let ease_0 = (std::f64::consts::PI * 0.0).sin();
let ease_1 = (std::f64::consts::PI * 1.0).sin();
assert!((ease_0).abs() < 0.001);
assert!((ease_1).abs() < 0.001);
}
#[test]
fn ease_function_max_at_midpoint() {
let ease = (std::f64::consts::PI * 0.5).sin();
assert!((ease - 1.0).abs() < 0.001);
}
#[test]
fn bezier_formula_degenerate_case_same_point() {
let x0 = 50.0;
let x1 = 50.0;
let cx1 = 50.0;
let cx2 = 50.0;
for i in 0..=10 {
let t = i as f64 / 10.0;
let u = 1.0 - t;
let x = u * u * u * x0 + 3.0 * u * u * t * cx1 + 3.0 * u * t * t * cx2 + t * t * t * x1;
assert!((x - 50.0).abs() < 0.001);
}
}
}