"""cloakbrowser-human — Configuration and presets. All numeric parameters for human-like behavior are centralized here. Two built-in presets: 'default' (normal human speed) and 'careful' (slower, more cautious). """ from __future__ import annotations import math import random import time from dataclasses import dataclass, field from typing import Literal, Tuple, TypedDict # --------------------------------------------------------------------------- # Type alias # --------------------------------------------------------------------------- Range = Tuple[float, float] HumanPreset = Literal["default", "careful"] class HumanConfigOverrides(TypedDict, total=False): typing_delay: float typing_delay_spread: float typing_pause_chance: float typing_pause_range: Range shift_down_delay: Range shift_up_delay: Range key_hold: Range field_switch_delay: Range mistype_chance: float mistype_delay_notice: Range mistype_delay_correct: Range mouse_steps_divisor: float mouse_min_steps: int mouse_max_steps: int mouse_wobble_max: float mouse_overshoot_chance: float mouse_overshoot_px: Range mouse_burst_size: Range mouse_burst_pause: Range click_aim_delay_input: Range click_aim_delay_button: Range click_hold_input: Range click_hold_button: Range click_input_x_range: Range idle_drift_px: float idle_pause_range: Range scroll_delta_base: Range scroll_delta_variance: float scroll_pause_fast: Range scroll_pause_slow: Range scroll_accel_steps: Range scroll_decel_steps: Range scroll_overshoot_chance: float scroll_overshoot_px: Range scroll_settle_delay: Range scroll_target_zone: Range scroll_pre_move_delay: Range initial_cursor_x: Range initial_cursor_y: Range idle_between_actions: bool idle_between_duration: Range # --------------------------------------------------------------------------- # Configuration dataclass # --------------------------------------------------------------------------- @dataclass class HumanConfig: """All tunable parameters for human-like behavior.""" # Keyboard typing_delay: float = 70 typing_delay_spread: float = 40 typing_pause_chance: float = 0.1 typing_pause_range: Range = (400, 1000) shift_down_delay: Range = (30, 70) shift_up_delay: Range = (20, 50) key_hold: Range = (15, 35) # Mistype (typo simulation) mistype_chance: float = 0.02 mistype_delay_notice: Range = (100, 300) mistype_delay_correct: Range = (50, 150) field_switch_delay: Range = (800, 1500) # Mouse — movement mouse_steps_divisor: float = 8 mouse_min_steps: int = 25 mouse_max_steps: int = 80 mouse_wobble_max: float = 1.5 mouse_overshoot_chance: float = 0.15 mouse_overshoot_px: Range = (3, 6) mouse_burst_size: Range = (3, 5) mouse_burst_pause: Range = (8, 18) # Mouse — clicks click_aim_delay_input: Range = (60, 140) click_aim_delay_button: Range = (80, 200) click_hold_input: Range = (40, 100) click_hold_button: Range = (60, 150) click_input_x_range: Range = (0.05, 0.30) # Mouse — idle idle_drift_px: float = 3 idle_pause_range: Range = (300, 1000) # Scroll scroll_delta_base: Range = (80, 130) scroll_delta_variance: float = 0.2 scroll_pause_fast: Range = (30, 80) scroll_pause_slow: Range = (80, 200) scroll_accel_steps: Range = (2, 3) scroll_decel_steps: Range = (2, 3) scroll_overshoot_chance: float = 0.1 scroll_overshoot_px: Range = (50, 150) scroll_settle_delay: Range = (300, 600) scroll_target_zone: Range = (0.20, 0.80) scroll_pre_move_delay: Range = (100, 300) # Initial cursor position (as if coming from the address bar area) initial_cursor_x: Range = (400, 700) initial_cursor_y: Range = (45, 60) # Idle micro-movements between actions (opt-in, adds latency) idle_between_actions: bool = False idle_between_duration: Range = (0.3, 0.8) # --------------------------------------------------------------------------- # Presets # --------------------------------------------------------------------------- def _careful_config() -> HumanConfig: """Careful preset — everything slower and more deliberate.""" return HumanConfig( # Keyboard — slower typing typing_delay=100, typing_delay_spread=50, typing_pause_chance=0.15, typing_pause_range=(500, 1200), shift_down_delay=(40, 90), shift_up_delay=(30, 70), key_hold=(20, 45), field_switch_delay=(1000, 2000), # Mouse — slower, more precise mouse_overshoot_chance=0.10, mouse_burst_pause=(12, 25), # Mouse — clicks (longer aiming and holding) click_aim_delay_input=(80, 180), click_aim_delay_button=(120, 280), click_hold_input=(60, 140), click_hold_button=(80, 200), # Scroll — slower scroll_pause_fast=(100, 200), scroll_pause_slow=(250, 600), scroll_settle_delay=(400, 800), scroll_pre_move_delay=(150, 400), # Idle between actions enabled for careful preset idle_between_actions=True, idle_between_duration=(0.4, 1.0), ) _PRESETS: dict[str, HumanConfig] = { "default": HumanConfig(), "careful": _careful_config(), } def resolve_config( preset: HumanPreset = "default", overrides: HumanConfigOverrides | None = None, ) -> HumanConfig: """Resolve a preset name + optional overrides into a full HumanConfig. Args: preset: 'default' or 'careful'. overrides: Typed mapping of HumanConfig field names to override values. Returns: A new HumanConfig instance. Raises: ValueError: If preset is not a recognized name. """ if preset not in _PRESETS: raise ValueError( f"Unknown humanize preset {preset!r}. " f"Valid presets: {', '.join(sorted(_PRESETS.keys()))}" ) base = _PRESETS[preset] if not overrides: return HumanConfig(**{k: getattr(base, k) for k in base.__dataclass_fields__}) merged = {k: getattr(base, k) for k in base.__dataclass_fields__} merged.update(overrides) return HumanConfig(**merged) def merge_config(base: HumanConfig, overrides: dict | None) -> HumanConfig: """Merge ``overrides`` (a dict of HumanConfig field names → values) on top of ``base``. Returns a new HumanConfig — ``base`` is never mutated. Used by per-call overrides like ``page.type(sel, text, human_config={...})`` so the same page can use different timings for different inputs without re-patching. Unknown keys are ignored silently to keep this forgiving for callers. """ if not overrides: return base merged = {k: getattr(base, k) for k in base.__dataclass_fields__} for k, v in overrides.items(): if k in base.__dataclass_fields__: merged[k] = v return HumanConfig(**merged) # --------------------------------------------------------------------------- # Utility functions # --------------------------------------------------------------------------- def rand(lo: float, hi: float) -> float: """Random float in [lo, hi].""" return random.uniform(lo, hi) def rand_int(lo: int, hi: int) -> int: """Random integer in [lo, hi] inclusive.""" return random.randint(lo, hi) def rand_range(r: Range) -> float: """Random float from a (min, max) tuple.""" return random.uniform(r[0], r[1]) def rand_int_range(r: Range) -> int: """Random integer from a (min, max) tuple, inclusive.""" return random.randint(int(r[0]), int(r[1])) def sleep_ms(ms: float) -> None: """Sleep for `ms` milliseconds.""" if ms > 0: time.sleep(ms / 1000.0) async def async_sleep_ms(ms: float) -> None: """Async sleep for `ms` milliseconds.""" if ms > 0: import asyncio await asyncio.sleep(ms / 1000.0)