mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
Bezier mouse curves, per-character typing with mistype simulation, smooth micro-step scrolling, idle micro-movements between actions. Supports both sync and async Playwright APIs. Patches page, frame, context, browser, and Locator class methods. Two presets: 'default' (normal speed) and 'careful' (slower, deliberate). Configurable via HumanConfig dataclass / interface with full override support. Bug fixes (from PR review): - fill()/clear(): platform-aware select-all (Meta+a on macOS, Control+a elsewhere) - sync Locator check()/uncheck(): wrap mouse_move in RawMouse-compatible object - resolve_config(): raise error on unknown preset name - Lazy-load human.config via __getattr__ in __init__.py - humanPreset typed as 'default' | 'careful' literal union - browser.newPage() patches implicit context Tests: Python 36/36, JS Vitest 34/34, visual Python 17/17, JS 13/13
88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""cloakbrowser-human — Async human-like mouse movement and clicking.
|
|
|
|
Mirrors mouse.py but uses ``await`` for all Playwright calls and
|
|
``async_sleep_ms`` instead of ``sleep_ms``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import random
|
|
from typing import Any, Protocol
|
|
|
|
from .config import HumanConfig, rand, rand_range, rand_int_range, async_sleep_ms
|
|
from .mouse import Point, _ease_in_out, _bezier, _random_control_points, click_target # noqa: reuse pure math
|
|
|
|
|
|
class AsyncRawMouse(Protocol):
|
|
async def move(self, x: float, y: float) -> None: ...
|
|
async def down(self) -> None: ...
|
|
async def up(self) -> None: ...
|
|
async def wheel(self, delta_x: float, delta_y: float) -> None: ...
|
|
|
|
|
|
async def async_human_move(
|
|
raw: AsyncRawMouse,
|
|
start_x: float, start_y: float,
|
|
end_x: float, end_y: float,
|
|
cfg: HumanConfig,
|
|
) -> None:
|
|
dist = math.hypot(end_x - start_x, end_y - start_y)
|
|
if dist < 1:
|
|
return
|
|
|
|
steps = max(cfg.mouse_min_steps, min(cfg.mouse_max_steps, round(dist / cfg.mouse_steps_divisor)))
|
|
start = Point(start_x, start_y)
|
|
end = Point(end_x, end_y)
|
|
cp1, cp2 = _random_control_points(start, end)
|
|
|
|
burst_counter = 0
|
|
burst_size = rand_int_range(cfg.mouse_burst_size)
|
|
|
|
for i in range(steps + 1):
|
|
progress = i / steps
|
|
eased_t = _ease_in_out(progress)
|
|
pt = _bezier(start, cp1, cp2, end, eased_t)
|
|
|
|
wobble_amp = math.sin(math.pi * progress) * cfg.mouse_wobble_max
|
|
wx = pt.x + (random.random() - 0.5) * 2 * wobble_amp
|
|
wy = pt.y + (random.random() - 0.5) * 2 * wobble_amp
|
|
|
|
await raw.move(round(wx), round(wy))
|
|
|
|
burst_counter += 1
|
|
if burst_counter >= burst_size and i < steps:
|
|
await async_sleep_ms(rand_range(cfg.mouse_burst_pause))
|
|
burst_counter = 0
|
|
|
|
if random.random() < cfg.mouse_overshoot_chance:
|
|
overshoot_dist = rand_range(cfg.mouse_overshoot_px)
|
|
angle = math.atan2(end_y - start_y, end_x - start_x)
|
|
await raw.move(round(end_x + math.cos(angle) * overshoot_dist),
|
|
round(end_y + math.sin(angle) * overshoot_dist))
|
|
await async_sleep_ms(rand(30, 70))
|
|
await raw.move(round(end_x + (random.random() - 0.5) * 4),
|
|
round(end_y + (random.random() - 0.5) * 4))
|
|
|
|
|
|
async def async_human_click(raw: AsyncRawMouse, is_input: bool, cfg: HumanConfig) -> None:
|
|
aim_delay = rand_range(cfg.click_aim_delay_input) if is_input else rand_range(cfg.click_aim_delay_button)
|
|
await async_sleep_ms(aim_delay)
|
|
hold_time = rand_range(cfg.click_hold_input) if is_input else rand_range(cfg.click_hold_button)
|
|
await raw.down()
|
|
await async_sleep_ms(hold_time)
|
|
await raw.up()
|
|
|
|
|
|
async def async_human_idle(raw: AsyncRawMouse, seconds: float, cx: float, cy: float, cfg: HumanConfig) -> None:
|
|
import time as _time
|
|
end_time = _time.monotonic() + seconds
|
|
x, y = cx, cy
|
|
while _time.monotonic() < end_time:
|
|
dx = (random.random() - 0.5) * 2 * cfg.idle_drift_px
|
|
dy = (random.random() - 0.5) * 2 * cfg.idle_drift_px
|
|
x += dx
|
|
y += dy
|
|
await raw.move(round(x), round(y))
|
|
await async_sleep_ms(rand_range(cfg.idle_pause_range))
|