133 lines
4.1 KiB
Python
133 lines
4.1 KiB
Python
"""cloakbrowser-human — Human-like mouse movement and clicking."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import random
|
|
from typing import Any, Protocol, Tuple
|
|
|
|
from .config import HumanConfig, rand, rand_range, rand_int_range, sleep_ms
|
|
|
|
|
|
class RawMouse(Protocol):
|
|
def move(self, x: float, y: float) -> None: ...
|
|
def down(self) -> None: ...
|
|
def up(self) -> None: ...
|
|
def wheel(self, delta_x: float, delta_y: float) -> None: ...
|
|
|
|
|
|
class Point:
|
|
__slots__ = ("x", "y")
|
|
def __init__(self, x: float, y: float):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
|
|
def _ease_in_out(t: float) -> float:
|
|
if t < 0.5:
|
|
return 4 * t * t * t
|
|
return 1 - pow(-2 * t + 2, 3) / 2
|
|
|
|
|
|
def _bezier(p0: Point, p1: Point, p2: Point, p3: Point, t: float) -> Point:
|
|
u = 1 - t
|
|
uu = u * u
|
|
uuu = uu * u
|
|
tt = t * t
|
|
ttt = tt * t
|
|
return Point(
|
|
uuu * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + ttt * p3.x,
|
|
uuu * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + ttt * p3.y,
|
|
)
|
|
|
|
|
|
def _random_control_points(start: Point, end: Point) -> Tuple[Point, Point]:
|
|
dx = end.x - start.x
|
|
dy = end.y - start.y
|
|
dist = math.hypot(dx, dy) or 1
|
|
px = -dy / dist
|
|
py = dx / dist
|
|
bias1 = rand(-0.3, 0.3) * dist
|
|
bias2 = rand(-0.3, 0.3) * dist
|
|
return (
|
|
Point(start.x + dx * 0.25 + px * bias1, start.y + dy * 0.25 + py * bias1),
|
|
Point(start.x + dx * 0.75 + px * bias2, start.y + dy * 0.75 + py * bias2),
|
|
)
|
|
|
|
|
|
def human_move(
|
|
raw: RawMouse,
|
|
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
|
|
|
|
raw.move(round(wx), round(wy))
|
|
|
|
burst_counter += 1
|
|
if burst_counter >= burst_size and i < steps:
|
|
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)
|
|
raw.move(round(end_x + math.cos(angle) * overshoot_dist),
|
|
round(end_y + math.sin(angle) * overshoot_dist))
|
|
sleep_ms(rand(30, 70))
|
|
raw.move(round(end_x + (random.random() - 0.5) * 4),
|
|
round(end_y + (random.random() - 0.5) * 4))
|
|
|
|
|
|
def click_target(box: dict, is_input: bool, cfg: HumanConfig) -> Point:
|
|
if is_input:
|
|
x_frac = rand_range(cfg.click_input_x_range)
|
|
y_frac = rand(0.30, 0.70)
|
|
else:
|
|
x_frac = rand(0.35, 0.65)
|
|
y_frac = rand(0.35, 0.65)
|
|
return Point(round(box["x"] + box["width"] * x_frac),
|
|
round(box["y"] + box["height"] * y_frac))
|
|
|
|
|
|
def human_click(raw: RawMouse, 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)
|
|
sleep_ms(aim_delay)
|
|
hold_time = rand_range(cfg.click_hold_input) if is_input else rand_range(cfg.click_hold_button)
|
|
raw.down()
|
|
sleep_ms(hold_time)
|
|
raw.up()
|
|
|
|
|
|
def human_idle(raw: RawMouse, 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
|
|
raw.move(round(x), round(y))
|
|
sleep_ms(rand_range(cfg.idle_pause_range))
|