"""Human-like behavior patterns for input simulation. Provides randomization utilities to make bot inputs appear natural, including variable delays, mouse jitter, and activity scheduling. """ import random import time import logging from typing import Tuple, Optional from dataclasses import dataclass, field logger = logging.getLogger(__name__) @dataclass class HumanProfile: """Defines a human behavior profile for input randomization.""" # Reaction time range in seconds reaction_min: float = 0.15 reaction_max: float = 0.45 # Mouse movement speed range (pixels per second) mouse_speed_min: float = 400.0 mouse_speed_max: float = 1200.0 # Click position jitter in pixels click_jitter: int = 3 # Chance of double-reading (hesitation before action) hesitation_chance: float = 0.1 hesitation_duration: Tuple[float, float] = (0.3, 1.2) # Break scheduling micro_break_interval: Tuple[int, int] = (120, 300) # seconds micro_break_duration: Tuple[int, int] = (2, 8) # seconds long_break_interval: Tuple[int, int] = (1800, 3600) # seconds long_break_duration: Tuple[int, int] = (60, 300) # seconds class Humanizer: """Applies human-like randomization to bot actions.""" def __init__(self, profile: Optional[HumanProfile] = None): self.profile = profile or HumanProfile() self._last_micro_break = time.time() self._last_long_break = time.time() self._next_micro_break = self._schedule_break(self.profile.micro_break_interval) self._next_long_break = self._schedule_break(self.profile.long_break_interval) self._action_count = 0 def reaction_delay(self) -> float: """Generate a human-like reaction delay.""" base = random.uniform(self.profile.reaction_min, self.profile.reaction_max) # Occasionally add hesitation if random.random() < self.profile.hesitation_chance: base += random.uniform(*self.profile.hesitation_duration) # Slight fatigue factor based on actions performed fatigue = min(self._action_count / 1000, 0.3) base *= (1 + fatigue * random.random()) return base def jitter_position(self, x: int, y: int) -> Tuple[int, int]: """Add small random offset to click position.""" jitter = self.profile.click_jitter return ( x + random.randint(-jitter, jitter), y + random.randint(-jitter, jitter), ) def mouse_speed(self) -> float: """Get randomized mouse movement speed.""" return random.uniform( self.profile.mouse_speed_min, self.profile.mouse_speed_max, ) def should_take_break(self) -> Optional[float]: """Check if it's time for a break. Returns break duration or None.""" now = time.time() if now >= self._next_long_break: duration = random.uniform(*self.profile.long_break_duration) self._next_long_break = now + duration + self._schedule_break( self.profile.long_break_interval ) logger.info(f"Long break: {duration:.0f}s") return duration if now >= self._next_micro_break: duration = random.uniform(*self.profile.micro_break_duration) self._next_micro_break = now + duration + self._schedule_break( self.profile.micro_break_interval ) logger.debug(f"Micro break: {duration:.1f}s") return duration return None def wait(self) -> None: """Wait for a human-like reaction delay.""" delay = self.reaction_delay() time.sleep(delay) self._action_count += 1 def _schedule_break(self, interval: Tuple[int, int]) -> float: """Schedule next break with randomized interval.""" return time.time() + random.uniform(*interval)