mirror of
https://git.zavage.net/Zavage-Software/wabot.git
synced 2026-07-21 13:06:08 -06:00
feat: pacing policies (HumanPacing default behavior, NoPacing for tests)
This commit is contained in:
parent
afa47186cc
commit
25aaa5d4da
51
src/wabot/pacing.py
Normal file
51
src/wabot/pacing.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""Pacing policies: how human-like the bot's interactions are.
|
||||||
|
|
||||||
|
``HumanPacing`` reproduces legacy wabot stealth behavior (random delays
|
||||||
|
between actions, Gaussian click offsets). ``NoPacing`` is instant and
|
||||||
|
exact — used by the test suite and for trusted-site automation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
|
||||||
|
class NoPacing:
|
||||||
|
"""Instant actions, exact clicks."""
|
||||||
|
|
||||||
|
def delay(self, action: str) -> float:
|
||||||
|
"""Seconds to sleep before performing ``action``."""
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def click_offset(self, width: int, height: int) -> tuple[int, int] | None:
|
||||||
|
"""Offset from the element center to click at, or None for a plain click."""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class HumanPacing(NoPacing):
|
||||||
|
"""Random delays and Gaussian click offsets (anti-bot stealth)."""
|
||||||
|
|
||||||
|
DELAYS: dict[str, tuple[float, float]] = {
|
||||||
|
"text": (3.0, 5.0),
|
||||||
|
"select": (6.0, 11.0),
|
||||||
|
"checkbox": (2.0, 3.0),
|
||||||
|
"navigate": (3.0, 6.0),
|
||||||
|
}
|
||||||
|
MIN_DIMENSION = 5 # elements thinner than this get a plain centered click
|
||||||
|
|
||||||
|
def delay(self, action: str) -> float:
|
||||||
|
lo, hi = self.DELAYS.get(action, (0.0, 0.0))
|
||||||
|
return random.uniform(lo, hi)
|
||||||
|
|
||||||
|
def click_offset(self, width: int, height: int) -> tuple[int, int] | None:
|
||||||
|
if width < self.MIN_DIMENSION or height < self.MIN_DIMENSION:
|
||||||
|
return None
|
||||||
|
return (self._axis_offset(width), self._axis_offset(height))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _axis_offset(size: int) -> int:
|
||||||
|
# Selenium 4 measures offsets from the element's in-view center, so a
|
||||||
|
# human-looking click is a Gaussian around 0 clamped inside the element.
|
||||||
|
max_offset = size // 2 - 1
|
||||||
|
offset = int(random.gauss(0.0, size / 7.0))
|
||||||
|
return max(-max_offset, min(max_offset, offset))
|
||||||
40
tests/unit/test_pacing.py
Normal file
40
tests/unit/test_pacing.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from wabot.pacing import HumanPacing, NoPacing
|
||||||
|
|
||||||
|
|
||||||
|
class TestNoPacing:
|
||||||
|
def test_delay_is_zero_for_every_action(self):
|
||||||
|
p = NoPacing()
|
||||||
|
assert p.delay("text") == 0.0
|
||||||
|
assert p.delay("select") == 0.0
|
||||||
|
assert p.delay("unknown-action") == 0.0
|
||||||
|
|
||||||
|
def test_click_offset_is_none(self):
|
||||||
|
assert NoPacing().click_offset(200, 50) is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestHumanPacing:
|
||||||
|
def test_delays_fall_in_documented_ranges(self):
|
||||||
|
p = HumanPacing()
|
||||||
|
for _ in range(50):
|
||||||
|
assert 3.0 <= p.delay("text") <= 5.0
|
||||||
|
assert 6.0 <= p.delay("select") <= 11.0
|
||||||
|
assert 2.0 <= p.delay("checkbox") <= 3.0
|
||||||
|
assert 3.0 <= p.delay("navigate") <= 6.0
|
||||||
|
|
||||||
|
def test_unknown_action_has_no_delay(self):
|
||||||
|
assert HumanPacing().delay("unknown-action") == 0.0
|
||||||
|
|
||||||
|
def test_click_offset_stays_inside_element(self):
|
||||||
|
p = HumanPacing()
|
||||||
|
for _ in range(200):
|
||||||
|
offset = p.click_offset(100, 40)
|
||||||
|
assert offset is not None
|
||||||
|
x, y = offset
|
||||||
|
# offsets are measured from the element CENTER (selenium 4)
|
||||||
|
assert -49 <= x <= 49
|
||||||
|
assert -19 <= y <= 19
|
||||||
|
|
||||||
|
def test_tiny_elements_get_plain_click(self):
|
||||||
|
p = HumanPacing()
|
||||||
|
assert p.click_offset(4, 40) is None
|
||||||
|
assert p.click_offset(40, 4) is None
|
||||||
Loading…
Reference in New Issue
Block a user