112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""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)
|