/** * cloakbrowser-human — Human-like mouse movement and clicking. */ import { HumanConfig, rand, randRange, randIntRange, sleep } from './config.js'; // --------------------------------------------------------------------------- // Raw interface — original Playwright methods, bypassing the wrapper // --------------------------------------------------------------------------- export interface RawMouse { move: (x: number, y: number) => Promise; down: (options?: any) => Promise; up: (options?: any) => Promise; wheel: (deltaX: number, deltaY: number) => Promise; } export interface RawKeyboard { down: (key: string) => Promise; up: (key: string) => Promise; type: (text: string) => Promise; insertText: (text: string) => Promise; } // --------------------------------------------------------------------------- // Easing // --------------------------------------------------------------------------- function easeInOut(t: number): number { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; } // --------------------------------------------------------------------------- // Bezier // --------------------------------------------------------------------------- interface Point { x: number; y: number; } function bezier(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point { const u = 1 - t; const uu = u * u; const uuu = uu * u; const tt = t * t; const ttt = tt * t; return { x: uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x, y: uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y, }; } function randomControlPoints(start: Point, end: Point): [Point, Point] { const dx = end.x - start.x; const dy = end.y - start.y; const dist = Math.hypot(dx, dy); const px = -dy / (dist || 1); const py = dx / (dist || 1); const bias1 = rand(-0.3, 0.3) * dist; const bias2 = rand(-0.3, 0.3) * dist; return [ { x: start.x + dx * 0.25 + px * bias1, y: start.y + dy * 0.25 + py * bias1 }, { x: start.x + dx * 0.75 + px * bias2, y: start.y + dy * 0.75 + py * bias2 }, ]; } // --------------------------------------------------------------------------- // Human mouse movement // --------------------------------------------------------------------------- export async function humanMove( raw: RawMouse, startX: number, startY: number, endX: number, endY: number, cfg: HumanConfig, ): Promise { const dist = Math.hypot(endX - startX, endY - startY); if (dist < 1) return; const steps = Math.max( cfg.mouse_min_steps, Math.min(cfg.mouse_max_steps, Math.round(dist / cfg.mouse_steps_divisor)), ); const start: Point = { x: startX, y: startY }; const end: Point = { x: endX, y: endY }; const [cp1, cp2] = randomControlPoints(start, end); let burstCounter = 0; const burstSize = randIntRange(cfg.mouse_burst_size); for (let i = 0; i <= steps; i++) { const progress = i / steps; const easedT = easeInOut(progress); const pt = bezier(start, cp1, cp2, end, easedT); const wobbleAmp = Math.sin(Math.PI * progress) * cfg.mouse_wobble_max; const wx = pt.x + (Math.random() - 0.5) * 2 * wobbleAmp; const wy = pt.y + (Math.random() - 0.5) * 2 * wobbleAmp; await raw.move(Math.round(wx), Math.round(wy)); burstCounter++; if (burstCounter >= burstSize && i < steps) { await sleep(randRange(cfg.mouse_burst_pause)); burstCounter = 0; } } if (Math.random() < cfg.mouse_overshoot_chance) { const overshootDist = randRange(cfg.mouse_overshoot_px); const angle = Math.atan2(endY - startY, endX - startX); const ovX = Math.round(endX + Math.cos(angle) * overshootDist); const ovY = Math.round(endY + Math.sin(angle) * overshootDist); await raw.move(ovX, ovY); await sleep(rand(30, 70)); const corrX = Math.round(endX + (Math.random() - 0.5) * 4); const corrY = Math.round(endY + (Math.random() - 0.5) * 4); await raw.move(corrX, corrY); } } // --------------------------------------------------------------------------- // Human click // --------------------------------------------------------------------------- export function clickTarget( box: { x: number; y: number; width: number; height: number }, isInput: boolean, cfg: HumanConfig, ): Point { if (isInput) { const xFrac = randRange(cfg.click_input_x_range); const yFrac = rand(0.30, 0.70); return { x: Math.round(box.x + box.width * xFrac), y: Math.round(box.y + box.height * yFrac), }; } const xFrac = rand(0.35, 0.65); const yFrac = rand(0.35, 0.65); return { x: Math.round(box.x + box.width * xFrac), y: Math.round(box.y + box.height * yFrac), }; } export async function humanClick( raw: RawMouse, isInput: boolean, cfg: HumanConfig, ): Promise { const aimDelay = isInput ? randRange(cfg.click_aim_delay_input) : randRange(cfg.click_aim_delay_button); await sleep(aimDelay); const holdTime = isInput ? randRange(cfg.click_hold_input) : randRange(cfg.click_hold_button); await raw.down(); await sleep(holdTime); await raw.up(); } // --------------------------------------------------------------------------- // Human idle / drift // --------------------------------------------------------------------------- export function humanIdle( raw: RawMouse, cx: number, cy: number, cfg: HumanConfig, ): Promise; export function humanIdle( raw: RawMouse, seconds: number, cx: number, cy: number, cfg: HumanConfig, ): Promise; export async function humanIdle( raw: RawMouse, secondsOrCx: number, cxOrCy: number, cyOrCfg: number | HumanConfig, maybeCfg?: HumanConfig, ): Promise { const hasExplicitSeconds = maybeCfg !== undefined; const seconds = hasExplicitSeconds ? secondsOrCx : rand((cyOrCfg as HumanConfig).idle_between_duration[0], (cyOrCfg as HumanConfig).idle_between_duration[1]); const cx = hasExplicitSeconds ? cxOrCy : secondsOrCx; const cy = hasExplicitSeconds ? (cyOrCfg as number) : cxOrCy; const cfg = hasExplicitSeconds ? maybeCfg! : (cyOrCfg as HumanConfig); const endTime = Date.now() + seconds * 1000; let x = cx; let y = cy; while (Date.now() < endTime) { const dx = (Math.random() - 0.5) * 2 * cfg.idle_drift_px; const dy = (Math.random() - 0.5) * 2 * cfg.idle_drift_px; x += dx; y += dy; await raw.move(Math.round(x), Math.round(y)); await sleep(randRange(cfg.idle_pause_range)); } }