use crate::render::chrome::page::Page;
use crate::render::chrome_protocol::cdp::browser_protocol::input::{
DispatchKeyEventParams, DispatchKeyEventType, DispatchMouseEventParams, DispatchMouseEventType,
InsertTextParams, MouseButton,
};
use crate::render::chrome_protocol::cdp::js_protocol::runtime::EvaluateParams;
use crate::render::keyboard::{KeyEvent, TypingEngine};
use crate::render::motion::lifecycle::{HIDE_SNIPPET, SHOW_SNIPPET};
use crate::render::motion::scroll::{schedule_for_active_profile as scroll_schedule, ScrollParams};
use crate::render::motion::{
fatigue, idle::IdleState, scroll as scroll_mod, MotionEngine, MotionProfile, Point, TimedPoint,
};
use rand::rngs::SmallRng;
use rand::RngExt;
use serde::Deserialize;
use std::sync::Arc;
use std::time::Duration;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
async fn element_rect(page: &Page, selector: &str) -> Result<Option<Rect>> {
crate::render::selector::resolve_rect(page, selector).await
}
#[derive(Clone, Copy, Debug, Default)]
pub struct MousePos {
pub x: f64,
pub y: f64,
}
async fn dispatch_move(page: &Page, x: f64, y: f64) -> Result<()> {
let params = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseMoved)
.x(x)
.y(y)
.build()
.map_err(|e| Error::Render(format!("mouse params: {e}")))?;
page.execute(params)
.await
.map_err(|e| Error::Render(format!("mouse move: {e}")))?;
Ok(())
}
async fn walk_trajectory(page: &Page, pts: &[TimedPoint]) -> Result<()> {
for p in pts {
dispatch_move(page, p.x, p.y).await?;
if p.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(p.delay_ms)).await;
}
}
Ok(())
}
pub async fn mouse_move_to(page: &Page, from: MousePos, x: f64, y: f64) -> Result<MousePos> {
let profile = MotionProfile::active();
let mut engine = MotionEngine::new(profile);
let pts = engine.trajectory(
Point {
x: from.x,
y: from.y,
},
Point { x, y },
40.0,
);
walk_trajectory(page, &pts).await?;
Ok(MousePos { x, y })
}
pub async fn click_selector(page: &Page, css: &str, from: MousePos) -> Result<MousePos> {
let rect = element_rect(page, css)
.await?
.ok_or_else(|| Error::Render(format!("element not found: {css}")))?;
let mut rng = rand::make_rng::<SmallRng>();
let tx = rect.x + rect.w * rng.random_range(0.25..0.75);
let ty = rect.y + rect.h * rng.random_range(0.25..0.75);
click_point(page, from, tx, ty, rect.w.min(rect.h).max(10.0)).await
}
pub async fn click_point(
page: &Page,
from: MousePos,
x: f64,
y: f64,
target_width: f64,
) -> Result<MousePos> {
let profile = MotionProfile::active();
let params = profile.params();
let mut engine = MotionEngine::new(profile);
let pts = engine.trajectory(
Point {
x: from.x,
y: from.y,
},
Point { x, y },
target_width,
);
walk_trajectory(page, &pts).await?;
if params.emit_mouseover {
dispatch_move(page, x, y).await?;
}
let mut rng = rand::make_rng::<SmallRng>();
let pause = u64_range(
&mut rng,
params.post_move_pause_ms_min,
params.post_move_pause_ms_max,
);
if pause > 0 {
tokio::time::sleep(Duration::from_millis(pause)).await;
}
let press = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MousePressed)
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(1)
.build()
.map_err(|e| Error::Render(format!("click params: {e}")))?;
page.execute(press)
.await
.map_err(|e| Error::Render(format!("click: {e}")))?;
let hold = u64_range(
&mut rng,
params.mouse_down_pause_ms_min,
params.mouse_down_pause_ms_max,
);
if hold > 0 {
tokio::time::sleep(Duration::from_millis(hold)).await;
}
let release = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseReleased)
.x(x)
.y(y)
.button(MouseButton::Left)
.click_count(1)
.build()
.map_err(|e| Error::Render(format!("click params: {e}")))?;
page.execute(release)
.await
.map_err(|e| Error::Render(format!("click: {e}")))?;
Ok(MousePos { x, y })
}
fn u64_range(rng: &mut SmallRng, lo: u64, hi: u64) -> u64 {
if hi <= lo {
return lo;
}
rng.random_range(lo..hi)
}
pub async fn type_text(page: &Page, selector: &str, text: &str) -> Result<()> {
if !crate::render::selector::focus(page, selector).await? {
return Err(Error::Render(format!("focus failed: {selector}")));
}
dispatch_typing(page, text).await
}
pub async fn dispatch_typing(page: &Page, text: &str) -> Result<()> {
let profile = MotionProfile::active();
let mut engine = TypingEngine::new(profile);
let events = engine.schedule(text);
for ev in events {
match ev {
KeyEvent::Pause { ms } => {
if ms > 0 {
tokio::time::sleep(Duration::from_millis(ms)).await;
}
}
KeyEvent::Char { ch, hold_ms } => {
dispatch_char(page, ch, hold_ms).await?;
}
KeyEvent::Typo { wrong, hold_ms } => {
dispatch_char(page, wrong, hold_ms).await?;
}
KeyEvent::Backspace { hold_ms } => {
let down = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyDown)
.key("Backspace".to_string())
.code("Backspace".to_string())
.build()
.map_err(|e| Error::Render(format!("backspace params: {e}")))?;
page.execute(down)
.await
.map_err(|e| Error::Render(format!("backspace: {e}")))?;
if hold_ms > 0 {
tokio::time::sleep(Duration::from_millis(hold_ms)).await;
}
let up = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyUp)
.key("Backspace".to_string())
.code("Backspace".to_string())
.build()
.map_err(|e| Error::Render(format!("backspace params: {e}")))?;
page.execute(up)
.await
.map_err(|e| Error::Render(format!("backspace: {e}")))?;
}
}
}
Ok(())
}
async fn dispatch_char(page: &Page, ch: char, hold_ms: u64) -> Result<()> {
if ch.is_ascii() && !ch.is_control() {
let text = ch.to_string();
let down = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyDown)
.text(text.clone())
.build()
.map_err(|e| Error::Render(format!("key params: {e}")))?;
page.execute(down)
.await
.map_err(|e| Error::Render(format!("key: {e}")))?;
if hold_ms > 0 {
tokio::time::sleep(Duration::from_millis(hold_ms)).await;
}
let up = DispatchKeyEventParams::builder()
.r#type(DispatchKeyEventType::KeyUp)
.text(text)
.build()
.map_err(|e| Error::Render(format!("key params: {e}")))?;
page.execute(up)
.await
.map_err(|e| Error::Render(format!("key: {e}")))?;
} else {
let p = InsertTextParams::builder()
.text(ch.to_string())
.build()
.map_err(|e| Error::Render(format!("insert params: {e}")))?;
page.execute(p)
.await
.map_err(|e| Error::Render(format!("insert: {e}")))?;
if hold_ms > 0 {
tokio::time::sleep(Duration::from_millis(hold_ms)).await;
}
}
Ok(())
}
pub async fn scroll_by(page: &Page, dy: f64, from: MousePos) -> Result<()> {
let ticks = scroll_schedule(dy);
for tick in ticks {
if tick.delta_y.abs() >= 0.5 {
let params = DispatchMouseEventParams::builder()
.r#type(DispatchMouseEventType::MouseWheel)
.x(from.x.max(10.0))
.y(from.y.max(10.0))
.delta_x(0.0)
.delta_y(tick.delta_y)
.build()
.map_err(|e| Error::Render(format!("wheel params: {e}")))?;
page.execute(params)
.await
.map_err(|e| Error::Render(format!("wheel: {e}")))?;
}
if tick.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(tick.delay_ms)).await;
}
}
Ok(())
}
pub async fn emit_page_hidden(page: &Page) -> Result<()> {
let _ = eval_js(page, HIDE_SNIPPET).await?;
Ok(())
}
pub async fn emit_page_visible(page: &Page) -> Result<()> {
let _ = eval_js(page, SHOW_SNIPPET).await?;
Ok(())
}
pub fn spawn_idle_drift(page: std::sync::Arc<Page>, origin: MousePos, state: Arc<IdleState>) {
let profile = MotionProfile::active();
if matches!(profile, MotionProfile::Fast) {
return;
}
let seed = rand::make_rng::<SmallRng>().random::<u64>();
tokio::spawn(async move {
let mut drift = crate::render::motion::IdleDrift::for_profile(profile, seed);
for _ in 0..21_600u64 {
if state.is_action_active() {
tokio::time::sleep(Duration::from_millis(200)).await;
continue;
}
let (dx, dy) = drift.next_offset();
let x = (origin.x + dx).max(1.0);
let y = (origin.y + dy).max(1.0);
if dispatch_move(&page, x, y).await.is_err() {
return;
}
let sleep_ms = drift.next_delay_ms();
tokio::time::sleep(Duration::from_millis(sleep_ms)).await;
}
});
}
pub fn scroll_params_with_fatigue() -> ScrollParams {
let mut p = ScrollParams::for_profile(MotionProfile::active());
let factor = fatigue::current_velocity_factor();
p.peak_tick_px *= factor;
p
}
#[allow(dead_code)]
fn _assert_scroll_mod_linked() -> usize {
std::mem::size_of::<scroll_mod::ScrollTick>()
}
pub async fn wait_for_selector(page: &Page, css: &str, timeout_ms: u64) -> Result<()> {
let deadline = std::time::Instant::now() + Duration::from_millis(timeout_ms);
loop {
if element_rect(page, css).await?.is_some() {
return Ok(());
}
if std::time::Instant::now() >= deadline {
return Err(Error::Render(format!("wait_for_selector timeout: {css}")));
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
}
pub async fn eval_js(page: &Page, script: &str) -> Result<serde_json::Value> {
let params = EvaluateParams::builder()
.expression(script.to_string())
.return_by_value(true)
.await_promise(true)
.build()
.map_err(|e| Error::Render(format!("eval params: {e}")))?;
let res = page
.evaluate_expression(params)
.await
.map_err(|e| Error::Render(format!("eval: {e}")))?;
Ok(res.value().cloned().unwrap_or(serde_json::Value::Null))
}