Spaces:
Sleeping
Sleeping
| """ | |
| Модуль для имитации человеческого поведения | |
| Это Python-модуль (не JS!) для реалистичного взаимодействия с браузером. | |
| Используется для обхода поведенческого анализа AWS FWCIM. | |
| Особенности: | |
| - Случайные опечатки и исправления | |
| - Паузы "на подумать" перед вводом | |
| - Разная скорость для разных типов полей | |
| - Реалистичные движения мыши по кривой Безье | |
| """ | |
| import random | |
| import time | |
| import string | |
| class BehaviorSpoofModule: | |
| """ | |
| Имитация человеческого поведения при взаимодействии с браузером. | |
| Использование: | |
| behavior = BehaviorSpoofModule() | |
| behavior.human_delay() # Пауза между действиями | |
| behavior.human_type(element, "text", field_type="email") # Печать с задержками | |
| """ | |
| name = "behavior" | |
| description = "Human-like behavior simulation (Python)" | |
| # Типы полей и их характеристики скорости | |
| FIELD_SPEEDS = { | |
| 'email': {'delay': (0.03, 0.08), 'typo_prob': 0.01}, # Email - быстро, мало ошибок (знакомый текст) | |
| 'password': {'delay': (0.08, 0.18), 'typo_prob': 0.0}, # Пароль - медленнее, БЕЗ опечаток (критично!) | |
| 'name': {'delay': (0.05, 0.12), 'typo_prob': 0.01}, # Имя - средняя скорость | |
| 'code': {'delay': (0.12, 0.25), 'typo_prob': 0.0}, # Код верификации - БЕЗ опечаток! | |
| 'default': {'delay': (0.05, 0.15), 'typo_prob': 0.01} # По умолчанию | |
| } | |
| # Соседние клавиши для реалистичных опечаток | |
| NEARBY_KEYS = { | |
| 'q': 'wa', 'w': 'qeas', 'e': 'wrsd', 'r': 'etdf', 't': 'ryfg', | |
| 'y': 'tugh', 'u': 'yihj', 'i': 'uojk', 'o': 'iplk', 'p': 'ol', | |
| 'a': 'qwsz', 's': 'awedxz', 'd': 'serfcx', 'f': 'drtgvc', 'g': 'ftyhbv', | |
| 'h': 'gyujnb', 'j': 'huikmn', 'k': 'jiolm', 'l': 'kop', | |
| 'z': 'asx', 'x': 'zsdc', 'c': 'xdfv', 'v': 'cfgb', 'b': 'vghn', | |
| 'n': 'bhjm', 'm': 'njk', | |
| '1': '2q', '2': '13qw', '3': '24we', '4': '35er', '5': '46rt', | |
| '6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p' | |
| } | |
| def __init__(self): | |
| # Настройки задержек | |
| self.typing_delay_range = (0.05, 0.15) # Между символами | |
| self.action_delay_range = (0.3, 1.0) # Между действиями | |
| self.think_delay_range = (0.5, 2.0) # "Думает" перед действием | |
| # Вероятности | |
| self.typo_probability = 0.02 # Вероятность опечатки | |
| self.pause_probability = 0.1 # Вероятность паузы при печати | |
| # Статистика сессии (для более реалистичного поведения) | |
| self._chars_typed = 0 | |
| self._typos_made = 0 | |
| self._session_start = time.time() | |
| def human_delay(self, min_delay: float = None, max_delay: float = None): | |
| """Человеческая задержка между действиями""" | |
| min_d = min_delay or self.action_delay_range[0] | |
| max_d = max_delay or self.action_delay_range[1] | |
| time.sleep(random.uniform(min_d, max_d)) | |
| def think_delay(self): | |
| """Задержка "размышления" перед действием""" | |
| time.sleep(random.uniform(*self.think_delay_range)) | |
| def typing_delay(self): | |
| """Задержка между нажатиями клавиш""" | |
| delay = random.uniform(*self.typing_delay_range) | |
| # Иногда делаем паузу | |
| if random.random() < self.pause_probability: | |
| delay += random.uniform(0.3, 0.8) | |
| time.sleep(delay) | |
| def simulate_reading(self, duration: float = None): | |
| """Симулирует чтение страницы""" | |
| if duration is None: | |
| duration = random.uniform(1.0, 3.0) | |
| time.sleep(duration) | |
| def human_type(self, element, text: str, clear_first: bool = True, field_type: str = 'default'): | |
| """ | |
| Печатает текст с человеческими задержками. | |
| Args: | |
| element: Элемент для ввода (DrissionPage element) | |
| text: Текст для ввода | |
| clear_first: Очистить поле перед вводом | |
| field_type: Тип поля ('email', 'password', 'name', 'code', 'default') | |
| """ | |
| # Получаем настройки для типа поля | |
| field_config = self.FIELD_SPEEDS.get(field_type, self.FIELD_SPEEDS['default']) | |
| delay_range = field_config['delay'] | |
| typo_prob = field_config['typo_prob'] | |
| # Пауза "на подумать" перед вводом (особенно для паролей и кодов) | |
| if field_type in ('password', 'code'): | |
| self.think_before_typing(field_type) | |
| element.click() | |
| self.human_delay(0.1, 0.3) | |
| if clear_first: | |
| element.clear() | |
| self.human_delay(0.1, 0.2) | |
| i = 0 | |
| while i < len(text): | |
| char = text[i] | |
| # Случайная пауза "на подумать" в середине ввода | |
| if random.random() < 0.03 and i > 0 and i < len(text) - 1: | |
| time.sleep(random.uniform(0.3, 0.8)) | |
| # Опечатка с реалистичным исправлением | |
| if random.random() < typo_prob and i < len(text) - 1: | |
| typo_char = self._get_typo_char(char) | |
| if typo_char: | |
| element.input(typo_char) | |
| self._chars_typed += 1 | |
| self._typos_made += 1 | |
| # Задержка перед осознанием ошибки | |
| time.sleep(random.uniform(0.1, 0.4)) | |
| # Иногда печатаем ещё 1-2 символа перед исправлением | |
| extra_chars = 0 | |
| if random.random() < 0.3 and i + 1 < len(text): | |
| extra_chars = random.randint(1, min(2, len(text) - i - 1)) | |
| for j in range(extra_chars): | |
| element.input(text[i + 1 + j]) | |
| time.sleep(random.uniform(*delay_range)) | |
| # Пауза "заметили ошибку" | |
| time.sleep(random.uniform(0.2, 0.5)) | |
| # Удаляем ошибочные символы | |
| for _ in range(1 + extra_chars): | |
| element.input('\b') | |
| time.sleep(random.uniform(0.05, 0.1)) | |
| # Вводим правильный символ | |
| element.input(char) | |
| self._chars_typed += 1 | |
| # Задержка между символами | |
| delay = random.uniform(*delay_range) | |
| # Дополнительная пауза после определённых символов | |
| if char in '.,!?@': | |
| delay += random.uniform(0.1, 0.3) | |
| elif char == ' ': | |
| delay += random.uniform(0.05, 0.15) | |
| time.sleep(delay) | |
| i += 1 | |
| def _get_typo_char(self, char: str) -> str | None: | |
| """Возвращает реалистичную опечатку для символа""" | |
| char_lower = char.lower() | |
| # Используем соседние клавиши | |
| if char_lower in self.NEARBY_KEYS: | |
| nearby = self.NEARBY_KEYS[char_lower] | |
| typo = random.choice(nearby) | |
| # Сохраняем регистр | |
| return typo.upper() if char.isupper() else typo | |
| # Для других символов - случайная буква | |
| if char.isalpha(): | |
| typo = random.choice(string.ascii_lowercase) | |
| return typo.upper() if char.isupper() else typo | |
| return None | |
| def think_before_typing(self, field_type: str = 'default'): | |
| """ | |
| Пауза "на подумать" перед вводом. | |
| Разная длительность для разных типов полей. | |
| """ | |
| if field_type == 'password': | |
| # Вспоминаем пароль | |
| time.sleep(random.uniform(0.8, 2.0)) | |
| elif field_type == 'code': | |
| # Смотрим на код в письме/SMS | |
| time.sleep(random.uniform(1.0, 2.5)) | |
| elif field_type == 'email': | |
| # Email обычно помним хорошо | |
| time.sleep(random.uniform(0.2, 0.5)) | |
| else: | |
| time.sleep(random.uniform(0.3, 0.8)) | |
| def human_click(self, element, pre_delay: bool = True): | |
| """Кликает с человеческой задержкой""" | |
| if pre_delay: | |
| self.human_delay(0.2, 0.5) | |
| element.click() | |
| self.human_delay(0.1, 0.3) | |
| def human_js_click(self, page, element, pre_delay: bool = True): | |
| """Кликает через JS с человеческой задержкой и скроллом""" | |
| if pre_delay: | |
| self.human_delay(0.15, 0.4) | |
| try: | |
| # Скроллим к элементу плавно | |
| page.run_js(''' | |
| arguments[0].scrollIntoView({behavior: "smooth", block: "center"}); | |
| ''', element) | |
| self.human_delay(0.1, 0.25) | |
| # Клик | |
| page.run_js('arguments[0].click()', element) | |
| except: | |
| try: | |
| element.click() | |
| except: | |
| pass | |
| self.human_delay(0.1, 0.3) | |
| def random_mouse_movement(self, browser, count: int = None): | |
| """ | |
| Случайные движения мыши по странице. | |
| Args: | |
| browser: BrowserAutomation instance (должен иметь .page) | |
| count: Количество движений | |
| """ | |
| if count is None: | |
| count = random.randint(2, 5) | |
| try: | |
| for _ in range(count): | |
| x = random.randint(100, 800) | |
| y = random.randint(100, 600) | |
| browser.page.run_js(f''' | |
| const event = new MouseEvent('mousemove', {{ | |
| clientX: {x}, | |
| clientY: {y}, | |
| bubbles: true | |
| }}); | |
| document.dispatchEvent(event); | |
| ''') | |
| time.sleep(random.uniform(0.1, 0.3)) | |
| except Exception: | |
| pass | |
| def scroll_page(self, browser, direction: str = 'down', amount: int = None): | |
| """ | |
| Прокручивает страницу. | |
| Args: | |
| browser: BrowserAutomation instance | |
| direction: 'up' или 'down' | |
| amount: Количество пикселей | |
| """ | |
| if amount is None: | |
| amount = random.randint(100, 400) | |
| if direction == 'up': | |
| amount = -amount | |
| try: | |
| browser.page.run_js(f'window.scrollBy(0, {amount});') | |
| self.human_delay(0.2, 0.5) | |
| except Exception: | |
| pass | |
| def bezier_mouse_move(self, page, start_x: int, start_y: int, end_x: int, end_y: int, steps: int = 20): | |
| """ | |
| Движение мыши по кривой Безье (более реалистично). | |
| Args: | |
| page: DrissionPage page instance | |
| start_x, start_y: Начальная позиция | |
| end_x, end_y: Конечная позиция | |
| steps: Количество шагов | |
| """ | |
| import math | |
| # Контрольные точки для кривой Безье | |
| # Добавляем случайное отклонение | |
| ctrl1_x = start_x + (end_x - start_x) * 0.3 + random.randint(-50, 50) | |
| ctrl1_y = start_y + (end_y - start_y) * 0.1 + random.randint(-30, 30) | |
| ctrl2_x = start_x + (end_x - start_x) * 0.7 + random.randint(-50, 50) | |
| ctrl2_y = start_y + (end_y - start_y) * 0.9 + random.randint(-30, 30) | |
| def bezier(t, p0, p1, p2, p3): | |
| """Кубическая кривая Безье""" | |
| return ( | |
| (1-t)**3 * p0 + | |
| 3 * (1-t)**2 * t * p1 + | |
| 3 * (1-t) * t**2 * p2 + | |
| t**3 * p3 | |
| ) | |
| try: | |
| for i in range(steps + 1): | |
| t = i / steps | |
| x = int(bezier(t, start_x, ctrl1_x, ctrl2_x, end_x)) | |
| y = int(bezier(t, start_y, ctrl1_y, ctrl2_y, end_y)) | |
| page.run_js(f''' | |
| const event = new MouseEvent('mousemove', {{ | |
| clientX: {x}, | |
| clientY: {y}, | |
| bubbles: true | |
| }}); | |
| document.dispatchEvent(event); | |
| ''') | |
| # Случайная задержка между шагами | |
| time.sleep(random.uniform(0.005, 0.02)) | |
| except Exception: | |
| pass | |
| def human_click_with_movement(self, page, element, from_pos: tuple = None): | |
| """ | |
| Кликает по элементу с реалистичным движением мыши. | |
| Args: | |
| page: DrissionPage page instance | |
| element: Элемент для клика | |
| from_pos: Начальная позиция (x, y), если None - случайная | |
| """ | |
| try: | |
| # Получаем позицию элемента | |
| rect = page.run_js(''' | |
| const rect = arguments[0].getBoundingClientRect(); | |
| return {x: rect.x + rect.width/2, y: rect.y + rect.height/2}; | |
| ''', element) | |
| end_x = int(rect['x']) | |
| end_y = int(rect['y']) | |
| # Начальная позиция | |
| if from_pos: | |
| start_x, start_y = from_pos | |
| else: | |
| start_x = random.randint(100, 800) | |
| start_y = random.randint(100, 600) | |
| # Движение к элементу | |
| self.bezier_mouse_move(page, start_x, start_y, end_x, end_y) | |
| # Небольшая пауза перед кликом | |
| time.sleep(random.uniform(0.05, 0.15)) | |
| # Клик | |
| element.click() | |
| return (end_x, end_y) # Возвращаем позицию для следующего движения | |
| except Exception as e: | |
| # Fallback на обычный клик | |
| element.click() | |
| return None | |
| # ======================================================================== | |
| # ADVANCED HUMAN SIMULATION | |
| # ======================================================================== | |
| def simulate_page_reading(self, page, duration: float = None): | |
| """ | |
| Симулирует чтение страницы: движения глаз (мыши), скролл, паузы. | |
| Args: | |
| page: DrissionPage instance | |
| duration: Длительность симуляции (None = случайная 2-5 сек) | |
| """ | |
| if duration is None: | |
| duration = random.uniform(2.0, 5.0) | |
| start_time = time.time() | |
| while time.time() - start_time < duration: | |
| action = random.choice(['mouse_move', 'scroll', 'pause']) | |
| if action == 'mouse_move': | |
| # Движение мыши как при чтении (сверху вниз, слева направо) | |
| x = random.randint(200, 900) | |
| y = random.randint(150, 500) | |
| try: | |
| page.run_js(f''' | |
| document.dispatchEvent(new MouseEvent('mousemove', {{ | |
| clientX: {x}, clientY: {y}, bubbles: true | |
| }})); | |
| ''') | |
| except: | |
| pass | |
| time.sleep(random.uniform(0.1, 0.3)) | |
| elif action == 'scroll': | |
| # Небольшой скролл | |
| scroll_amount = random.randint(50, 150) | |
| direction = random.choice([1, -1]) | |
| try: | |
| page.run_js(f'window.scrollBy(0, {scroll_amount * direction});') | |
| except: | |
| pass | |
| time.sleep(random.uniform(0.2, 0.5)) | |
| else: # pause | |
| time.sleep(random.uniform(0.3, 0.8)) | |
| def simulate_form_hesitation(self, page): | |
| """ | |
| Симулирует колебание перед заполнением формы. | |
| Человек обычно осматривает форму перед вводом. | |
| """ | |
| # Движения мыши по форме | |
| form_positions = [ | |
| (300, 200), (500, 200), (300, 300), (500, 300), (400, 400) | |
| ] | |
| for x, y in random.sample(form_positions, k=random.randint(2, 4)): | |
| x += random.randint(-30, 30) | |
| y += random.randint(-30, 30) | |
| try: | |
| page.run_js(f''' | |
| document.dispatchEvent(new MouseEvent('mousemove', {{ | |
| clientX: {x}, clientY: {y}, bubbles: true | |
| }})); | |
| ''') | |
| except: | |
| pass | |
| time.sleep(random.uniform(0.1, 0.25)) | |
| # Пауза "на подумать" | |
| time.sleep(random.uniform(0.3, 0.8)) | |
| def simulate_distraction(self, page, probability: float = 0.15): | |
| """ | |
| Симулирует отвлечение пользователя (с заданной вероятностью). | |
| Человек иногда отвлекается во время заполнения форм. | |
| Args: | |
| page: DrissionPage instance | |
| probability: Вероятность отвлечения (0.0 - 1.0) | |
| """ | |
| if random.random() > probability: | |
| return | |
| distraction_type = random.choice(['long_pause', 'scroll_away', 'mouse_wander']) | |
| if distraction_type == 'long_pause': | |
| # Просто долгая пауза (отвлёкся на телефон/чат) | |
| time.sleep(random.uniform(2.0, 5.0)) | |
| elif distraction_type == 'scroll_away': | |
| # Скролл в сторону и обратно | |
| try: | |
| page.run_js(f'window.scrollBy(0, {random.randint(100, 300)});') | |
| time.sleep(random.uniform(0.5, 1.5)) | |
| page.run_js(f'window.scrollBy(0, {random.randint(-300, -100)});') | |
| except: | |
| pass | |
| time.sleep(random.uniform(0.3, 0.6)) | |
| elif distraction_type == 'mouse_wander': | |
| # Мышь уходит в угол экрана | |
| corners = [(50, 50), (1200, 50), (50, 700), (1200, 700)] | |
| corner = random.choice(corners) | |
| try: | |
| page.run_js(f''' | |
| document.dispatchEvent(new MouseEvent('mousemove', {{ | |
| clientX: {corner[0]}, clientY: {corner[1]}, bubbles: true | |
| }})); | |
| ''') | |
| except: | |
| pass | |
| time.sleep(random.uniform(1.0, 3.0)) | |
| def hover_before_click(self, page, element, duration: float = None): | |
| """ | |
| Наводит мышь на элемент перед кликом (как реальный пользователь). | |
| Args: | |
| page: DrissionPage instance | |
| element: Элемент для наведения | |
| duration: Время наведения перед кликом | |
| """ | |
| if duration is None: | |
| duration = random.uniform(0.1, 0.4) | |
| try: | |
| # Получаем позицию элемента | |
| rect = page.run_js(''' | |
| const rect = arguments[0].getBoundingClientRect(); | |
| return { | |
| x: rect.x + rect.width/2 + (Math.random() - 0.5) * rect.width * 0.3, | |
| y: rect.y + rect.height/2 + (Math.random() - 0.5) * rect.height * 0.3 | |
| }; | |
| ''', element) | |
| # Hover event | |
| page.run_js(f''' | |
| const el = arguments[0]; | |
| el.dispatchEvent(new MouseEvent('mouseenter', {{ bubbles: true }})); | |
| el.dispatchEvent(new MouseEvent('mouseover', {{ bubbles: true }})); | |
| ''', element) | |
| time.sleep(duration) | |
| except: | |
| pass | |
| def natural_scroll_to_element(self, page, element): | |
| """ | |
| Плавно скроллит к элементу (не мгновенно). | |
| Args: | |
| page: DrissionPage instance | |
| element: Элемент к которому скроллить | |
| """ | |
| try: | |
| # Получаем текущую позицию и позицию элемента | |
| positions = page.run_js(''' | |
| const el = arguments[0]; | |
| const rect = el.getBoundingClientRect(); | |
| return { | |
| currentY: window.scrollY, | |
| targetY: window.scrollY + rect.top - window.innerHeight / 3, | |
| viewportHeight: window.innerHeight | |
| }; | |
| ''', element) | |
| current_y = positions['currentY'] | |
| target_y = positions['targetY'] | |
| # Скроллим пошагово | |
| distance = target_y - current_y | |
| steps = max(5, abs(int(distance / 50))) | |
| for i in range(steps): | |
| progress = (i + 1) / steps | |
| # Easing function (ease-out) | |
| eased = 1 - (1 - progress) ** 2 | |
| new_y = current_y + distance * eased | |
| page.run_js(f'window.scrollTo(0, {int(new_y)});') | |
| time.sleep(random.uniform(0.02, 0.05)) | |
| # Финальная позиция | |
| page.run_js(f'window.scrollTo(0, {int(target_y)});') | |
| time.sleep(random.uniform(0.1, 0.2)) | |
| except Exception: | |
| # Fallback на обычный скролл | |
| try: | |
| page.run_js('arguments[0].scrollIntoView({behavior: "smooth", block: "center"});', element) | |
| time.sleep(0.3) | |
| except: | |
| pass | |
| def random_micro_movements(self, page, count: int = None): | |
| """ | |
| Микро-движения мыши (тремор руки, небольшие корректировки). | |
| Делает поведение более человечным. | |
| Args: | |
| page: DrissionPage instance | |
| count: Количество микро-движений | |
| """ | |
| if count is None: | |
| count = random.randint(3, 8) | |
| try: | |
| # Получаем текущую позицию (примерно центр экрана) | |
| base_x = random.randint(400, 800) | |
| base_y = random.randint(300, 500) | |
| for _ in range(count): | |
| # Небольшое отклонение (1-5 пикселей) | |
| dx = random.randint(-5, 5) | |
| dy = random.randint(-5, 5) | |
| page.run_js(f''' | |
| document.dispatchEvent(new MouseEvent('mousemove', {{ | |
| clientX: {base_x + dx}, | |
| clientY: {base_y + dy}, | |
| bubbles: true | |
| }})); | |
| ''') | |
| time.sleep(random.uniform(0.02, 0.08)) | |
| base_x += dx | |
| base_y += dy | |
| except: | |
| pass | |
| # ======================================================================== | |
| # FWCIM-COMPATIBLE INPUT (Critical for AWS detection bypass) | |
| # ======================================================================== | |
| def fwcim_type(self, page, element, text: str, field_type: str = 'default', fast: bool = False): | |
| """ | |
| Печатает текст с генерацией правильных событий для FWCIM. | |
| FWCIM собирает: | |
| - keyCycles: [{startEventTime, endEventTime, which}, ...] - время удержания клавиши | |
| - keyPressTimeIntervals: время между нажатиями | |
| - keyPresses: количество нажатий | |
| Args: | |
| page: DrissionPage instance | |
| element: Элемент для ввода | |
| text: Текст для ввода | |
| field_type: Тип поля ('email', 'password', 'name', 'code', 'default') | |
| fast: Быстрый режим (меньше задержек, но всё ещё генерирует события) | |
| """ | |
| # Получаем настройки для типа поля | |
| field_config = self.FIELD_SPEEDS.get(field_type, self.FIELD_SPEEDS['default']) | |
| delay_range = field_config['delay'] | |
| # В быстром режиме уменьшаем задержки | |
| if fast: | |
| delay_range = (0.01, 0.03) | |
| # Пауза "на подумать" перед вводом (только в медленном режиме) | |
| if not fast and field_type in ('password', 'code'): | |
| self.think_before_typing(field_type) | |
| # Фокус на элементе с правильными событиями | |
| self.fwcim_focus(page, element) | |
| time.sleep(random.uniform(0.05, 0.1) if fast else random.uniform(0.1, 0.2)) | |
| # Очищаем поле через select + delete (работает с React) | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| el.select(); | |
| ''', element) | |
| time.sleep(0.02) | |
| # Генерируем Delete/Backspace для очистки (FWCIM видит это) | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| if (el.value) { | |
| el.dispatchEvent(new KeyboardEvent('keydown', { | |
| key: 'Delete', code: 'Delete', keyCode: 46, which: 46, bubbles: true | |
| })); | |
| el.value = ''; | |
| el.dispatchEvent(new InputEvent('input', { | |
| inputType: 'deleteContentBackward', bubbles: true | |
| })); | |
| el.dispatchEvent(new KeyboardEvent('keyup', { | |
| key: 'Delete', code: 'Delete', keyCode: 46, which: 46, bubbles: true | |
| })); | |
| } | |
| ''', element) | |
| time.sleep(0.02) | |
| for i, char in enumerate(text): | |
| # Случайная пауза "на подумать" в середине ввода (только в медленном режиме) | |
| if not fast and random.random() < 0.03 and i > 0 and i < len(text) - 1: | |
| time.sleep(random.uniform(0.3, 0.8)) | |
| # Генерируем keydown -> keypress -> input -> keyup | |
| self._dispatch_key_events(page, element, char, fast=fast) | |
| # Задержка между символами (inter-key interval) | |
| delay = random.uniform(*delay_range) | |
| # Дополнительная пауза после определённых символов (только в медленном режиме) | |
| if not fast: | |
| if char in '.,!?@': | |
| delay += random.uniform(0.1, 0.3) | |
| elif char == ' ': | |
| delay += random.uniform(0.05, 0.15) | |
| time.sleep(delay) | |
| def _dispatch_key_events(self, page, element, char: str, fast: bool = False): | |
| """ | |
| Генерирует полную последовательность событий клавиши для FWCIM. | |
| Последовательность: keydown -> (hold time) -> keyup | |
| FWCIM записывает startEventTime (keydown) и endEventTime (keyup) | |
| ВАЖНО: Использует нативный setter для совместимости с React! | |
| """ | |
| # Время удержания клавиши (50-150ms для обычного набора, меньше в быстром режиме) | |
| hold_time = random.uniform(0.02, 0.05) if fast else random.uniform(0.05, 0.15) | |
| # Получаем keyCode для символа | |
| key_code = ord(char.upper()) if char.isalpha() else ord(char) | |
| # Определяем правильный code для клавиши | |
| if char.isalpha(): | |
| code = f'Key{char.upper()}' | |
| elif char.isdigit(): | |
| code = f'Digit{char}' | |
| elif char == ' ': | |
| code = 'Space' | |
| elif char == '@': | |
| code = 'Digit2' # Shift+2 на US keyboard | |
| elif char == '.': | |
| code = 'Period' | |
| elif char == '-': | |
| code = 'Minus' | |
| elif char == '_': | |
| code = 'Minus' # Shift+Minus | |
| else: | |
| code = f'Key{char}' | |
| # keydown + keypress + input (всё в одном JS вызове для атомарности) | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| const char = arguments[1]; | |
| const keyCode = arguments[2]; | |
| const code = arguments[3]; | |
| // keydown | |
| el.dispatchEvent(new KeyboardEvent('keydown', { | |
| key: char, | |
| code: code, | |
| keyCode: keyCode, | |
| which: keyCode, | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| // keypress (deprecated but FWCIM may still listen) | |
| el.dispatchEvent(new KeyboardEvent('keypress', { | |
| key: char, | |
| charCode: char.charCodeAt(0), | |
| keyCode: keyCode, | |
| which: keyCode, | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| // КРИТИЧНО: Используем нативный setter для React-совместимости | |
| // React перехватывает setter и обновляет state | |
| const nativeInputValueSetter = Object.getOwnPropertyDescriptor( | |
| window.HTMLInputElement.prototype, 'value' | |
| ).set; | |
| nativeInputValueSetter.call(el, el.value + char); | |
| // Input event (React слушает это) | |
| el.dispatchEvent(new InputEvent('input', { | |
| data: char, | |
| inputType: 'insertText', | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| ''', element, char, key_code, code) | |
| # Hold time (FWCIM measures this!) | |
| time.sleep(hold_time) | |
| # keyup event | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| const char = arguments[1]; | |
| const keyCode = arguments[2]; | |
| const code = arguments[3]; | |
| el.dispatchEvent(new KeyboardEvent('keyup', { | |
| key: char, | |
| code: code, | |
| keyCode: keyCode, | |
| which: keyCode, | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| ''', element, char, key_code, code) | |
| def fwcim_click(self, page, element): | |
| """ | |
| Кликает с генерацией правильных событий для FWCIM. | |
| FWCIM собирает: | |
| - mouseCycles: [{startEventTime, endEventTime}, ...] - время удержания клика | |
| - mouseClickPositions: позиции кликов | |
| - clicks: количество кликов | |
| """ | |
| # Время удержания кнопки мыши (80-200ms для обычного клика) | |
| hold_time = random.uniform(0.08, 0.2) | |
| # Получаем позицию элемента для реалистичного клика | |
| try: | |
| rect = page.run_js(''' | |
| const el = arguments[0]; | |
| const rect = el.getBoundingClientRect(); | |
| // Случайная позиция внутри элемента | |
| return { | |
| x: rect.left + rect.width * (0.3 + Math.random() * 0.4), | |
| y: rect.top + rect.height * (0.3 + Math.random() * 0.4) | |
| }; | |
| ''', element) | |
| client_x = int(rect['x']) | |
| client_y = int(rect['y']) | |
| except: | |
| client_x = 400 | |
| client_y = 300 | |
| # mousedown event | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| const x = arguments[1]; | |
| const y = arguments[2]; | |
| el.dispatchEvent(new MouseEvent('mousedown', { | |
| clientX: x, | |
| clientY: y, | |
| button: 0, | |
| buttons: 1, | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| ''', element, client_x, client_y) | |
| # Hold time (FWCIM measures this!) | |
| time.sleep(hold_time) | |
| # mouseup event | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| const x = arguments[1]; | |
| const y = arguments[2]; | |
| el.dispatchEvent(new MouseEvent('mouseup', { | |
| clientX: x, | |
| clientY: y, | |
| button: 0, | |
| buttons: 0, | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| // click event (follows mouseup) | |
| el.dispatchEvent(new MouseEvent('click', { | |
| clientX: x, | |
| clientY: y, | |
| button: 0, | |
| bubbles: true, | |
| cancelable: true | |
| })); | |
| ''', element, client_x, client_y) | |
| def fwcim_focus(self, page, element): | |
| """ | |
| Фокусируется на элементе с правильными событиями для FWCIM. | |
| FWCIM отслеживает: | |
| - firstFocusTime: время первого фокуса | |
| - totalFocusTime: общее время фокуса | |
| """ | |
| page.run_js(''' | |
| const el = arguments[0]; | |
| // focusin event (bubbles) | |
| el.dispatchEvent(new FocusEvent('focusin', { | |
| bubbles: true, | |
| cancelable: false | |
| })); | |
| // focus event (doesn't bubble) | |
| el.dispatchEvent(new FocusEvent('focus', { | |
| bubbles: false, | |
| cancelable: false | |
| })); | |
| // Actually focus the element | |
| el.focus(); | |
| ''', element) | |