├── LICENSE ├── README.md ├── humancursor ├── HCScripter │ ├── __init__.py │ ├── __pycache__ │ │ └── __init__.cpython-311.pyc │ ├── gui.py │ └── launch.py ├── __init__.py ├── system_cursor.py ├── test │ ├── system.py │ └── web.py ├── utilities │ ├── calculate_and_randomize.py │ ├── human_curve_generator.py │ └── web_adjuster.py └── web_cursor.py └── pyproject.toml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Flori Batusha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HumanCursor: A Python package for simulating human mouse movements 2 | 3 |
4 | 5 | 6 |
7 | 8 | _**HumanCursor**_ is a Python package that allows you to _**simulate realistic human mouse movements**_ on the web and the system. It can be used for _**automating scripts**_ that require mouse interactions, such as _**web scraping**_, _**automated tasks**_, _**testing**_, or _**gaming**_. 9 | 10 | # Content: 11 | 12 | - [Features](#features) 13 | - [Requirements](#requirements) 14 | - [How to install](#installation) 15 | - [How to use](#usage) 16 | - [HCScripter](#hcscripter) 17 | - [WebCursor](#webcursor) 18 | - [SystemCursor](#systemcursor) 19 | - [Demonstration](#demonstration) 20 | 21 | # Features 22 | 23 | - HumanCursor uses a `natural motion algorithm` that mimics the way `humans` move the mouse cursor, with `variable speed`, `acceleration`, and `curvature`. 24 | - Can perform various mouse actions, such as `clicking`, `dragging`, `scrolling`, and `hovering`. 25 | - Designed specifically to `bypass security measures and bot detection software`. 26 | - Includes: 27 | - 🚀 `HCScripter` app to create physical cursor automated scripts without coding. 28 | - 🌐 `WebCursor` module for web cursor code automation. 29 | - Fully supported for `Chrome` and `Edge`, not optimal/tested for Firefox and Safari, using `Selenium`. 30 | - 🤖 `SystemCursor` module for physical cursor code automation. 31 | 32 | 33 | # Requirements 34 | 35 | - ```Python >= 3.7``` 36 | - [Download the installer](https://www.python.org/downloads/), run it and follow the steps. 37 | - Make sure to check the box that says `Add Python to PATH` during installation. 38 | - Reboot computer. 39 | 40 | # Installation 41 | 42 | To install, you can use pip: 43 | 44 | pip install --upgrade humancursor 45 | 46 | # Usage 47 | 48 | ## HCScripter 49 | 50 | To quickly create an automated system script, you can use `HCScripter`, which registers mouse actions from point to point using key commands and creates a script file for you. 51 | 52 | After installing `humancursor` package, open up `terminal/powershell` and just copy paste this command which runs `launch.py` file inside the folder named `HCScripter` of `humancursor` package: 53 | 54 | ```powershell 55 | python -m humancursor.HCScripter.launch 56 | ``` 57 | 58 | #### A window will show up looking like this: 59 | 60 | Screenshot 2023-11-29 165810 61 | 62 | Firstly, you can specify the `name` of the python file which will contain the script and choose the `location` where that file should be saved. 63 | 64 | Then, you can turn on movement listener by pressing the `ON/OFF` button, where it will start registering your movements, by these commands below: 65 | 66 | - Press `Z` -> `Move` 67 | - Press `CTRL` -> `Click` 68 | - Press and hold `CTRL` -> `Drag and drop` 69 | 70 | After completing your script, press `Finish` button and the script file .py should be ready to go! 71 | 72 | ## WebCursor 73 | 74 | To use HumanCursor for Web, you need to import the `WebCursor` class, and create an instance: 75 | 76 | ```python 77 | from humancursor import WebCursor 78 | 79 | cursor = WebCursor(driver) 80 | ``` 81 | 82 | Then, you can use the following methods to simulate mouse movements and actions: 83 | 84 | - `cursor.move_to()`: Moves the mouse cursor to the element or location on the webpage. 85 | - `cursor.click_on()`: Clicks on the element or location on the webpage. 86 | - `cursor.drag_and_drop()`: Drags the mouse cursor from one element and drops it to another element on the screen. 87 | - `cursor.move_by_offset()`: Moves the cursor by x and y pixels. 88 | - `cursor.control_scroll_bar()`: Sets the scroll bar to a certain level, can be a volume, playback slider or anything. Level is set by float number from 0 to 1, meaning fullness 89 | - `cursor.scroll_into_view_of_element()`: Scrolls into view of element if not already there, it is called automatically from above functions. 90 | 91 | These functions can accept as destination, either the `WebElement` itself, or a `list of 'x' and 'y' coordinates`. 92 | 93 | Some parameters explained: 94 | 95 | - `relative_position`: Takes a list of x and y percentages as floats from 0 to 1, which indicate the exact position by width and height inside an element 96 | for example, if you set it to [0.5, 0.5], it will move the cursor to the center of the element. 97 | - `absolute_offset`: If you input a list of coordinates instead of webelement, if you turn this to True, the coordinates will be interpreted as absolute movement by pixels, and not like coordinates in the webpage. 98 | - `steady`: Tries to make movement in straight line, mimicking human, if set to True 99 | 100 | 101 | ## SystemCursor 102 |
103 | 104 | 105 | 106 |
107 | To use HumanCursor for your system mouse, you need to import the `SystemCursor` class, and create an instance just like we did above: 108 | 109 | ```python 110 | from humancursor import SystemCursor 111 | 112 | cursor = SystemCursor() 113 | ``` 114 | 115 | The `SystemCursor` class, which should be used for controlling the system mouse (with pyautogui), only inherits the `move_to()`, `click_on()` and `drag_and_drop` functions, accepting only the list of 'x' and 'y' coordinates as input, as there are no elements available. 116 | 117 | 118 | # DEMONSTRATION: 119 | To quickly check how the cursor moves, you can do this: 120 | 121 | #### SystemCursor 122 | ```powershell 123 | python -m humancursor.test.system 124 | ``` 125 | #### WebCursor 126 | ```powershell 127 | python -m humancursor.test.web 128 | ``` 129 | 130 | #### Some code examples: 131 | 132 | ```python 133 | cursor.move_to(element) # moves to element 134 | cursor.move_to(element, relative_position=[0.5, 0.5]) # moves to the center of the element 135 | cursor.move_to([450, 600]) # moves to coordinates relative to viewport x: 450, y: 600 136 | cursor.move_to([450, 600], absolute_offset=True) # moves 450 pixels to the right and 600 pixels down 137 | 138 | cursor.move_by_offset(200, 170) # moves 200 pixels to the right and 170 pixels down 139 | cursor.move_by_offset(-10, -20) # moves 10 pixels to the left and 20 pixels up 140 | 141 | cursor.click_on([170, 390]) # clicks on coordinates relative to viewport x: 170, y: 390 142 | cursor.click_on(element, relative_position=[0.2, 0.5]) # clicks on 0.2 x width, 0.5 x height position of the element. 143 | cursor.click_on(element, click_duration=1.7) # clicks and holds on element for 1.7 seconds 144 | 145 | cursor.drag_and_drop(element1, element2) # clicks and hold on first element, and moves to and releases on the second 146 | cursor.drag_and_drop(element, [640, 320], drag_from_relative_position=[0.9, 0.9]) # drags from element on 0.9 x width, 0.9 x height (far bottom right corner) and moves to and releases to coordinates relative to viewport x: 640, y: 320 147 | 148 | cursor.control_scroll_bar(element, amount_by_percentage=0.75) # sets a slider to 75% full 149 | cursor.controll_scroll_bar(element, amount_by_percentage=0.2, orientation='vertical') # sets a vertical slider to 20% full 150 | 151 | cursor.scroll_into_view_of_element(element) # scrolls into view of element if not already in it 152 | cursor.show_cursor() # injects javascript that will display a red dot over the cursor on webpage. Should be called only for visual testing before script and not actual work. 153 | 154 | ``` 155 | 156 | # License 157 | 158 | HumanCursor is licensed under the MIT License. See LICENSE for more information. 159 | -------------------------------------------------------------------------------- /humancursor/HCScripter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riflosnake/HumanCursor/39d33f387b953950ffc1035183c9609f7f87c2c8/humancursor/HCScripter/__init__.py -------------------------------------------------------------------------------- /humancursor/HCScripter/__pycache__/__init__.cpython-311.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riflosnake/HumanCursor/39d33f387b953950ffc1035183c9609f7f87c2c8/humancursor/HCScripter/__pycache__/__init__.cpython-311.pyc -------------------------------------------------------------------------------- /humancursor/HCScripter/gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import tkinter as tk 4 | from time import time 5 | from tkinter import ttk, filedialog 6 | 7 | import pyautogui 8 | 9 | 10 | class HCSWindow: 11 | def __init__(self): 12 | self.root = tk.Tk() 13 | self.root.title("HCS") 14 | self.coordinates = [] 15 | 16 | self.bg = '#3e99de' 17 | self.root.geometry("340x320") 18 | self.root.config(bg=self.bg) 19 | self.root.wm_attributes('-topmost', True) 20 | self.root.resizable(False, False) 21 | 22 | self.style = ttk.Style() 23 | self.style.configure('TLabel', font=('Roboto', 11)) 24 | 25 | self.label = ttk.Label(self.root, text="Cursor Position", background=self.bg) 26 | self.label.pack(pady=10) 27 | self.coordinates_label = ttk.Label(self.root, text="", background=self.bg) 28 | self.coordinates_label.pack(pady=10) 29 | 30 | self.file_name_label = ttk.Label(self.root, text="File Name (optional)", font=("Roboto", 10), background=self.bg) 31 | self.file_name_label.pack() 32 | self.file_name = ttk.Entry(self.root) 33 | self.file_name.pack(pady=5) 34 | 35 | self.destination_label = ttk.Label(self.root, text="File Destination", font=("Roboto", 10), background=self.bg) 36 | self.destination_label.pack() 37 | self.entry_var = tk.StringVar() 38 | self.destination = ttk.Entry(self.root, textvariable=self.entry_var, width=20) 39 | self.destination.pack(pady=5) 40 | 41 | self.browse_button = ttk.Button(self.root, text="Browse", command=self.browse_directory, style="TButton", width=6) 42 | self.browse_button.pack(anchor=tk.S, pady=3) 43 | 44 | self.activate_button = ttk.Button(self.root, text="ON/OFF", command=self.toggle_color, style="TButton") 45 | self.activate_button.pack(side=tk.LEFT, anchor=tk.S, padx=3, pady=3) 46 | 47 | self.confirm_button = ttk.Button(self.root, text="Finish", command=self.confirm, style="TButton") 48 | self.confirm_button.pack(side=tk.RIGHT, anchor=tk.S, padx=3, pady=3) 49 | 50 | self.indicator = tk.Canvas(self.root, width=50, height=100, background='#577b96') 51 | self.indicator.pack() 52 | 53 | self.indicator_color = "red" 54 | 55 | self.indicator.create_rectangle(15, 10, 38, 33, fill=self.indicator_color) 56 | 57 | self.root.bind("", self.remove_focus) 58 | self.activate_button.bind("", self.remove_focus) 59 | 60 | self.file = None 61 | self.dest = None 62 | 63 | self.ctrl_pressed = False 64 | self.press_time = 0.0 65 | self.index = -1 66 | 67 | self.hold_time_threshold = 0.5 68 | 69 | self.update_coordinates() 70 | self.root.mainloop() 71 | 72 | def __call__(self): 73 | # Returns these values when calling the instance of the HCSWindow object as a function 74 | return self.coordinates, self.file, self.dest 75 | 76 | def browse_directory(self): 77 | # Opens file browser 78 | folder_selected = filedialog.askdirectory() 79 | self.entry_var.set(folder_selected) 80 | 81 | def draw_indicator(self): 82 | # Draws the square determining status of registering movements 83 | self.indicator.delete("all") 84 | self.indicator.create_rectangle(15, 10, 38, 33, fill=self.indicator_color) 85 | 86 | def remove_focus(self, event): 87 | # Removes focus from field inputs when touching the window or buttons 88 | if event.widget != self.file_name and event.widget != self.destination: 89 | self.root.focus_force() 90 | 91 | def toggle_color(self): 92 | # Binding and unbinding of CTRL and Z to capture mouse coordinates 93 | if self.indicator_color == "red": 94 | self.root.bind("", self.on_press_ctrl) 95 | self.root.bind("", self.on_release_ctrl) 96 | self.root.bind("", self.move) 97 | self.indicator_color = "green" 98 | else: 99 | self.root.unbind("") 100 | self.root.unbind("") 101 | self.root.unbind("") 102 | self.indicator_color = "red" 103 | 104 | self.draw_indicator() 105 | 106 | def confirm(self): 107 | # Set return values before destroying window 108 | self.file = self.file_name.get() 109 | self.dest = self.destination.get() 110 | 111 | if not self.file: 112 | self.file = f'humancursor_{random.randint(1, 10000)}' 113 | 114 | if self.is_valid_file_location(self.dest): 115 | self.root.destroy() 116 | else: 117 | self.destination_label.config(background='red') 118 | 119 | @staticmethod 120 | def is_valid_file_location(file_path): 121 | # Checks if location path inputted manually exist 122 | return os.path.exists(file_path) 123 | 124 | def update_coordinates(self): 125 | # Update cursor coordinates every 10 milliseconds 126 | x, y = pyautogui.position() 127 | self.coordinates_label.config(text=f"x: {x}, y: {y}") 128 | self.root.after(10, self.update_coordinates) 129 | 130 | def move(self, event): 131 | # Appends 'Move' coordinates to all coordinates 132 | x, y = pyautogui.position() 133 | self.coordinates.append([x, y]) 134 | self.index += 1 135 | 136 | def on_press_ctrl(self, event): 137 | # Appends 'Click' coordinates to all coordinates 138 | if event.keysym == "Control_L" and not self.ctrl_pressed: 139 | self.ctrl_pressed = True 140 | x, y = pyautogui.position() 141 | self.press_time = time() 142 | self.coordinates.append([(x, y)]) 143 | self.index += 1 144 | 145 | def on_release_ctrl(self, event): 146 | # Updates existing 'Click' coordinates to destination if holds less than time threshold, or 147 | # Transforms existing 'Click' coordinates to 'Drag and Drop' coordinates if holds more than time threshold 148 | if event.keysym == "Control_L": 149 | self.ctrl_pressed = False 150 | x, y = pyautogui.position() 151 | if time() - self.press_time > self.hold_time_threshold: 152 | self.coordinates[self.index].append((x, y)) 153 | else: 154 | self.coordinates[self.index] = self.coordinates[self.index][0] 155 | self.press_time = 0.0 156 | -------------------------------------------------------------------------------- /humancursor/HCScripter/launch.py: -------------------------------------------------------------------------------- 1 | from humancursor.HCScripter.gui import HCSWindow 2 | 3 | mouse_coordinates, file_name, file_destination = HCSWindow()() 4 | 5 | imports = '# Importing SystemCursor from humancursor package\nfrom humancursor import SystemCursor\n\n' 6 | 7 | cursor = '# Initializing the SystemCursor object\ncursor = SystemCursor()\n\n' 8 | 9 | code = '# Script Recorded: \n\n' 10 | 11 | for coordinate in mouse_coordinates: 12 | if isinstance(coordinate, tuple): 13 | code += f'cursor.click_on({coordinate}, clicks=1, click_duration=0, steady=False)\n' 14 | elif isinstance(coordinate[0], int): 15 | code += f'cursor.move_to({coordinate}, duration=None, steady=False)\n' 16 | else: 17 | code += f'cursor.drag_and_drop({coordinate[0]}, {coordinate[1]}, duration=None, steady=False)\n' 18 | 19 | end = '\n# End\n\n' 20 | try: 21 | script_file = file_destination + '\\' + file_name + '.py' 22 | try: 23 | with open(script_file, 'w') as file: 24 | file.write(imports + cursor + code + end) 25 | except FileNotFoundError: 26 | print('File Not Found') 27 | except TypeError: 28 | pass 29 | -------------------------------------------------------------------------------- /humancursor/__init__.py: -------------------------------------------------------------------------------- 1 | from humancursor.system_cursor import SystemCursor 2 | from humancursor.web_cursor import WebCursor 3 | -------------------------------------------------------------------------------- /humancursor/system_cursor.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import random 3 | import pyautogui 4 | 5 | from humancursor.utilities.human_curve_generator import HumanizeMouseTrajectory 6 | from humancursor.utilities.calculate_and_randomize import generate_random_curve_parameters 7 | 8 | 9 | class SystemCursor: 10 | def __init__(self): 11 | pyautogui.MINIMUM_DURATION = 0 12 | pyautogui.MINIMUM_SLEEP = 0 13 | pyautogui.PAUSE = 0 14 | 15 | @staticmethod 16 | def move_to(point: list or tuple, duration: int or float = None, human_curve=None, steady=False): 17 | """Moves to certain coordinates of screen""" 18 | from_point = pyautogui.position() 19 | 20 | if not human_curve: 21 | ( 22 | offset_boundary_x, 23 | offset_boundary_y, 24 | knots_count, 25 | distortion_mean, 26 | distortion_st_dev, 27 | distortion_frequency, 28 | tween, 29 | target_points, 30 | ) = generate_random_curve_parameters( 31 | pyautogui, from_point, point 32 | ) 33 | if steady: 34 | offset_boundary_x, offset_boundary_y = 10, 10 35 | distortion_mean, distortion_st_dev, distortion_frequency = 1.2, 1.2, 1 36 | human_curve = HumanizeMouseTrajectory( 37 | from_point, 38 | point, 39 | offset_boundary_x=offset_boundary_x, 40 | offset_boundary_y=offset_boundary_y, 41 | knots_count=knots_count, 42 | distortion_mean=distortion_mean, 43 | distortion_st_dev=distortion_st_dev, 44 | distortion_frequency=distortion_frequency, 45 | tween=tween, 46 | target_points=target_points, 47 | ) 48 | 49 | if duration is None: 50 | duration = random.uniform(0.5, 2.0) 51 | pyautogui.PAUSE = duration / len(human_curve.points) 52 | for pnt in human_curve.points: 53 | pyautogui.moveTo(pnt) 54 | pyautogui.moveTo(point) 55 | 56 | def click_on(self, point: list or tuple, clicks: int = 1, click_duration: int or float = 0, steady=False): 57 | """Clicks a specified number of times, on the specified coordinates""" 58 | self.move_to(point, steady=steady) 59 | for _ in range(clicks): 60 | pyautogui.mouseDown() 61 | sleep(click_duration) 62 | pyautogui.mouseUp() 63 | sleep(random.uniform(0.170, 0.280)) 64 | 65 | def drag_and_drop(self, from_point: list or tuple, to_point: list or tuple, duration: int or float or [float, float] or (float, float) = None, steady=False): 66 | """Drags from a certain point, and releases to another""" 67 | if isinstance(duration, (list, tuple)): 68 | first_duration, second_duration = duration 69 | elif isinstance(duration, (float, int)): 70 | first_duration = second_duration = duration / 2 71 | else: 72 | first_duration = second_duration = None 73 | 74 | self.move_to(from_point, duration=first_duration) 75 | pyautogui.mouseDown() 76 | self.move_to(to_point, duration=second_duration, steady=steady) 77 | pyautogui.mouseUp() 78 | -------------------------------------------------------------------------------- /humancursor/test/system.py: -------------------------------------------------------------------------------- 1 | from humancursor.system_cursor import SystemCursor 2 | 3 | cursor = SystemCursor() # Initializing SystemCursor object 4 | 5 | 6 | # Just a couple movements to show how the cursor behaves 7 | 8 | def start_sys_demo(): 9 | print('Initializing System Demo') 10 | for _ in range(2): 11 | cursor.move_to([200, 200]) # Moving to coordinates x:200, y:200 12 | cursor.move_to([800, 200]) # Moving to coordinates x:800, y:200 13 | cursor.move_to([500, 800]) # Moving to coordinates x:500, y:800 14 | cursor.move_to([1400, 800]) # Moving to coordinates x:1400, y:800 15 | print('System Demo ended') 16 | 17 | 18 | start_sys_demo() 19 | -------------------------------------------------------------------------------- /humancursor/test/web.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from time import sleep 3 | 4 | from selenium import webdriver 5 | from selenium.webdriver.common.by import By 6 | 7 | from humancursor.web_cursor import WebCursor 8 | 9 | 10 | # Disclaimer: This script is for demonstration purposes only and should not be used for any cheating activity. 11 | 12 | def start_web_demo(): 13 | print('Initializing Web Demo') 14 | driver = webdriver.Chrome() # Creating an instance of driver 15 | cursor = WebCursor(driver) # Initializing WebCursor object 16 | 17 | driver.get('https://humanbenchmark.com/tests/chimp') # Going to this webpage 18 | driver.maximize_window() # Maximizing window 19 | cursor.show_cursor() # Injecting javascript to show a red dot over cursor 20 | 21 | sleep(1.5) 22 | 23 | start_button = driver.find_element(By.XPATH, 24 | '//button[text()="Start Test"]') # Getting element of Start Test Button 25 | 26 | cursor.click_on(start_button) # Clicking on Start Test Button 27 | 28 | sleep(1.2) 29 | 30 | for attempt in range(5): 31 | blocks = driver.find_elements(By.XPATH, '//div[@data-cellnumber]') # Finding all blocks 32 | blocks_sorted = sorted(blocks, 33 | key=lambda x: int(x.get_attribute('data-cellnumber'))) # Sorting them numerically 34 | for block in blocks_sorted: 35 | cursor.click_on(block) # Clicking on each block in order 36 | 37 | continue_button = driver.find_element(By.XPATH, 38 | '//button[text()="Continue"]') # Finding element of Continue Button 39 | cursor.click_on(continue_button) # Clicking on Continue Button 40 | sleep(1.2) 41 | 42 | sleep(3) 43 | print('Web Demo ended') 44 | driver.quit() # Quitting driver, closing window 45 | sys.exit() # End script 46 | 47 | 48 | start_web_demo() 49 | -------------------------------------------------------------------------------- /humancursor/utilities/calculate_and_randomize.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from selenium.webdriver import Chrome, Edge, Firefox, Safari 4 | import pytweening 5 | import random 6 | 7 | 8 | def calculate_absolute_offset(element, list_of_x_and_y_offsets): 9 | """Calculates exact number of pixel offsets from relative values""" 10 | dimensions = element.size 11 | width, height = dimensions["width"], dimensions["height"] 12 | x_final = width * list_of_x_and_y_offsets[0] 13 | y_final = height * list_of_x_and_y_offsets[1] 14 | 15 | return [int(x_final), int(y_final)] 16 | 17 | 18 | def generate_random_curve_parameters(driver, pre_origin, post_destination): 19 | """Generates random parameters for the curve, the tween, number of knots, distortion, target points and boundaries""" 20 | web = False 21 | if isinstance(driver, (Chrome, Firefox, Edge, Safari)): 22 | web = True 23 | viewport_width, viewport_height = driver.get_window_size().values() 24 | else: 25 | viewport_width, viewport_height = driver.size() 26 | min_width, max_width = viewport_width * 0.15, viewport_width * 0.85 27 | min_height, max_height = viewport_height * 0.15, viewport_height * 0.85 28 | 29 | tween_options = [ 30 | pytweening.easeOutExpo, 31 | pytweening.easeInOutQuint, 32 | pytweening.easeInOutSine, 33 | pytweening.easeInOutQuart, 34 | pytweening.easeInOutExpo, 35 | pytweening.easeInOutCubic, 36 | pytweening.easeInOutCirc, 37 | pytweening.linear, 38 | pytweening.easeOutSine, 39 | pytweening.easeOutQuart, 40 | pytweening.easeOutQuint, 41 | pytweening.easeOutCubic, 42 | pytweening.easeOutCirc, 43 | ] 44 | 45 | tween = random.choice(tween_options) 46 | offset_boundary_x = random.choice( 47 | random.choices( 48 | [range(20, 45), range(45, 75), range(75, 100)], [0.2, 0.65, 15] 49 | )[0] 50 | ) 51 | offset_boundary_y = random.choice( 52 | random.choices( 53 | [range(20, 45), range(45, 75), range(75, 100)], [0.2, 0.65, 15] 54 | )[0] 55 | ) 56 | knots_count = random.choices( 57 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 58 | [0.15, 0.36, 0.17, 0.12, 0.08, 0.04, 0.03, 0.02, 0.015, 0.005], 59 | )[0] 60 | 61 | distortion_mean = random.choice(range(80, 110)) / 100 62 | distortion_st_dev = random.choice(range(85, 110)) / 100 63 | distortion_frequency = random.choice(range(25, 70)) / 100 64 | 65 | if web: 66 | target_points = random.choice( 67 | random.choices( 68 | [range(35, 45), range(45, 60), range(60, 80)], [0.53, 0.32, 0.15] 69 | )[0] 70 | ) 71 | else: 72 | target_points = max(int(math.sqrt((pre_origin[0] - post_destination[0]) ** 2 + (pre_origin[1] - post_destination[1]) ** 2)), 2) 73 | 74 | if ( 75 | min_width > pre_origin[0] 76 | or max_width < pre_origin[0] 77 | or min_height > pre_origin[1] 78 | or max_height < pre_origin[1] 79 | ): 80 | offset_boundary_x = 1 81 | offset_boundary_y = 1 82 | knots_count = 1 83 | if ( 84 | min_width > post_destination[0] 85 | or max_width < post_destination[0] 86 | or min_height > post_destination[1] 87 | or max_height < post_destination[1] 88 | ): 89 | offset_boundary_x = 1 90 | offset_boundary_y = 1 91 | knots_count = 1 92 | 93 | return ( 94 | offset_boundary_x, 95 | offset_boundary_y, 96 | knots_count, 97 | distortion_mean, 98 | distortion_st_dev, 99 | distortion_frequency, 100 | tween, 101 | target_points, 102 | ) 103 | -------------------------------------------------------------------------------- /humancursor/utilities/human_curve_generator.py: -------------------------------------------------------------------------------- 1 | import random 2 | import math 3 | import numpy as np 4 | import pytweening 5 | 6 | 7 | class HumanizeMouseTrajectory: 8 | def __init__(self, from_point, to_point, **kwargs): 9 | self.from_point = from_point 10 | self.to_point = to_point 11 | self.points = self.generate_curve(**kwargs) 12 | 13 | def generate_curve(self, **kwargs): 14 | """Generates the curve based on arguments below, default values below are automatically modified to cause randomness""" 15 | offset_boundary_x = kwargs.get("offset_boundary_x", 80) 16 | offset_boundary_y = kwargs.get("offset_boundary_y", 80) 17 | left_boundary = ( 18 | kwargs.get("left_boundary", min(self.from_point[0], self.to_point[0])) 19 | - offset_boundary_x 20 | ) 21 | right_boundary = ( 22 | kwargs.get("right_boundary", max(self.from_point[0], self.to_point[0])) 23 | + offset_boundary_x 24 | ) 25 | down_boundary = ( 26 | kwargs.get("down_boundary", min(self.from_point[1], self.to_point[1])) 27 | - offset_boundary_y 28 | ) 29 | up_boundary = ( 30 | kwargs.get("up_boundary", max(self.from_point[1], self.to_point[1])) 31 | + offset_boundary_y 32 | ) 33 | knots_count = kwargs.get("knots_count", 2) 34 | distortion_mean = kwargs.get("distortion_mean", 1) 35 | distortion_st_dev = kwargs.get("distortion_st_dev", 1) 36 | distortion_frequency = kwargs.get("distortion_frequency", 0.5) 37 | tween = kwargs.get("tweening", pytweening.easeOutQuad) 38 | target_points = kwargs.get("target_points", 100) 39 | 40 | internalKnots = self.generate_internal_knots( 41 | left_boundary, right_boundary, down_boundary, up_boundary, knots_count 42 | ) 43 | points = self.generate_points(internalKnots) 44 | points = self.distort_points( 45 | points, distortion_mean, distortion_st_dev, distortion_frequency 46 | ) 47 | points = self.tween_points(points, tween, target_points) 48 | return points 49 | 50 | def generate_internal_knots( 51 | self, l_boundary, r_boundary, d_boundary, u_boundary, knots_count 52 | ): 53 | """Generates the internal knots of the curve randomly""" 54 | if not ( 55 | self.check_if_numeric(l_boundary) 56 | and self.check_if_numeric(r_boundary) 57 | and self.check_if_numeric(d_boundary) 58 | and self.check_if_numeric(u_boundary) 59 | ): 60 | raise ValueError("Boundaries must be numeric values") 61 | if not isinstance(knots_count, int) or knots_count < 0: 62 | knots_count = 0 63 | if l_boundary > r_boundary: 64 | raise ValueError( 65 | "left_boundary must be less than or equal to right_boundary" 66 | ) 67 | if d_boundary > u_boundary: 68 | raise ValueError( 69 | "down_boundary must be less than or equal to upper_boundary" 70 | ) 71 | try: 72 | knotsX = np.random.choice(range(l_boundary, r_boundary) or l_boundary, size=knots_count) 73 | knotsY = np.random.choice(range(d_boundary, u_boundary) or d_boundary, size=knots_count) 74 | except TypeError: 75 | knotsX = np.random.choice( 76 | range(int(l_boundary), int(r_boundary)), size=knots_count 77 | ) 78 | knotsY = np.random.choice( 79 | range(int(d_boundary), int(u_boundary)), size=knots_count 80 | ) 81 | knots = list(zip(knotsX, knotsY)) 82 | return knots 83 | 84 | def generate_points(self, knots): 85 | """Generates the points from BezierCalculator""" 86 | if not self.check_if_list_of_points(knots): 87 | raise ValueError("knots must be valid list of points") 88 | 89 | midPtsCnt = max( 90 | abs(self.from_point[0] - self.to_point[0]), 91 | abs(self.from_point[1] - self.to_point[1]), 92 | 2, 93 | ) 94 | knots = [self.from_point] + knots + [self.to_point] 95 | return BezierCalculator.calculate_points_in_curve(int(midPtsCnt), knots) 96 | 97 | def distort_points( 98 | self, points, distortion_mean, distortion_st_dev, distortion_frequency 99 | ): 100 | """Distorts points by parameters of mean, standard deviation and frequency""" 101 | if not ( 102 | self.check_if_numeric(distortion_mean) 103 | and self.check_if_numeric(distortion_st_dev) 104 | and self.check_if_numeric(distortion_frequency) 105 | ): 106 | raise ValueError("Distortions must be numeric") 107 | if not self.check_if_list_of_points(points): 108 | raise ValueError("points must be valid list of points") 109 | if not (0 <= distortion_frequency <= 1): 110 | raise ValueError("distortion_frequency must be in range [0,1]") 111 | 112 | distorted = [] 113 | for i in range(1, len(points) - 1): 114 | x, y = points[i] 115 | delta = ( 116 | np.random.normal(distortion_mean, distortion_st_dev) 117 | if random.random() < distortion_frequency 118 | else 0 119 | ) 120 | distorted += ((x, y + delta),) 121 | distorted = [points[0]] + distorted + [points[-1]] 122 | return distorted 123 | 124 | def tween_points(self, points, tween, target_points): 125 | """Modifies points by tween""" 126 | if not self.check_if_list_of_points(points): 127 | raise ValueError("List of points not valid") 128 | if not isinstance(target_points, int) or target_points < 2: 129 | raise ValueError("target_points must be an integer greater or equal to 2") 130 | 131 | res = [] 132 | for i in range(target_points): 133 | index = int(tween(float(i) / (target_points - 1)) * (len(points) - 1)) 134 | res += (points[index],) 135 | return res 136 | 137 | @staticmethod 138 | def check_if_numeric(val): 139 | """Checks if value is proper numeric value""" 140 | return isinstance(val, (float, int, np.int32, np.int64, np.float32, np.float64)) 141 | 142 | def check_if_list_of_points(self, list_of_points): 143 | """Checks if list of points is valid""" 144 | if not isinstance(list_of_points, list): 145 | return False 146 | try: 147 | point = lambda p: ( 148 | (len(p) == 2) 149 | and self.check_if_numeric(p[0]) 150 | and self.check_if_numeric(p[1]) 151 | ) 152 | return all(map(point, list_of_points)) 153 | except (KeyError, TypeError): 154 | return False 155 | 156 | 157 | class BezierCalculator: 158 | @staticmethod 159 | def binomial(n, k): 160 | """Returns the binomial coefficient "n choose k" """ 161 | return math.factorial(n) / float(math.factorial(k) * math.factorial(n - k)) 162 | 163 | @staticmethod 164 | def bernstein_polynomial_point(x, i, n): 165 | """Calculate the i-th component of a bernstein polynomial of degree n""" 166 | return BezierCalculator.binomial(n, i) * (x**i) * ((1 - x) ** (n - i)) 167 | 168 | @staticmethod 169 | def bernstein_polynomial(points): 170 | """ 171 | Given list of control points, returns a function, which given a point [0,1] returns 172 | a point in the Bézier curve described by these points 173 | """ 174 | 175 | def bernstein(t): 176 | n = len(points) - 1 177 | x = y = 0 178 | for i, point in enumerate(points): 179 | bern = BezierCalculator.bernstein_polynomial_point(t, i, n) 180 | x += point[0] * bern 181 | y += point[1] * bern 182 | return x, y 183 | 184 | return bernstein 185 | 186 | @staticmethod 187 | def calculate_points_in_curve(n, points): 188 | """ 189 | Given list of control points, returns n points in the Bézier curve, 190 | described by these points 191 | """ 192 | curvePoints = [] 193 | bernstein_polynomial = BezierCalculator.bernstein_polynomial(points) 194 | for i in range(n): 195 | t = i / (n - 1) 196 | curvePoints += (bernstein_polynomial(t),) 197 | return curvePoints 198 | -------------------------------------------------------------------------------- /humancursor/utilities/web_adjuster.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from selenium.common.exceptions import MoveTargetOutOfBoundsException 4 | from selenium.webdriver.common.action_chains import ActionChains 5 | from selenium.webdriver import Firefox 6 | 7 | from humancursor.utilities.human_curve_generator import HumanizeMouseTrajectory 8 | from humancursor.utilities.calculate_and_randomize import generate_random_curve_parameters, calculate_absolute_offset 9 | 10 | 11 | class WebAdjuster: 12 | def __init__(self, driver): 13 | self.__driver = driver 14 | self.__action = ActionChains(self.__driver, duration=0 if not isinstance(driver, Firefox) else 1) 15 | self.origin_coordinate = [0, 0] 16 | 17 | def move_to( 18 | self, 19 | element_or_pos, 20 | origin_coordinates=None, 21 | absolute_offset=False, 22 | relative_position=None, 23 | human_curve=None, 24 | steady=False 25 | ): 26 | """Moves the cursor, trying to mimic human behaviour!""" 27 | origin = origin_coordinates 28 | if origin_coordinates is None: 29 | origin = self.origin_coordinate 30 | 31 | pre_origin = tuple(origin) 32 | if isinstance(element_or_pos, list): 33 | if not absolute_offset: 34 | x, y = element_or_pos[0], element_or_pos[1] 35 | else: 36 | x, y = ( 37 | element_or_pos[0] + pre_origin[0], 38 | element_or_pos[1] + pre_origin[1], 39 | ) 40 | else: 41 | script = "return { x: Math.round(arguments[0].getBoundingClientRect().left), y: Math.round(arguments[0].getBoundingClientRect().top) };" 42 | destination = self.__driver.execute_script(script, element_or_pos) 43 | if relative_position is None: 44 | x_random_off = random.choice(range(20, 80)) / 100 45 | y_random_off = random.choice(range(20, 80)) / 100 46 | 47 | x, y = destination["x"] + ( 48 | element_or_pos.size["width"] * x_random_off 49 | ), destination["y"] + (element_or_pos.size["height"] * y_random_off) 50 | else: 51 | abs_exact_offset = calculate_absolute_offset( 52 | element_or_pos, relative_position 53 | ) 54 | x_exact_off, y_exact_off = abs_exact_offset[0], abs_exact_offset[1] 55 | x, y = destination["x"] + x_exact_off, destination["y"] + y_exact_off 56 | 57 | ( 58 | offset_boundary_x, 59 | offset_boundary_y, 60 | knots_count, 61 | distortion_mean, 62 | distortion_st_dev, 63 | distortion_frequency, 64 | tween, 65 | target_points, 66 | ) = generate_random_curve_parameters( 67 | self.__driver, [origin[0], origin[1]], [x, y] 68 | ) 69 | if steady: 70 | offset_boundary_x, offset_boundary_y = 10, 10 71 | distortion_mean, distortion_st_dev, distortion_frequency = 1.2, 1.2, 1 72 | if not human_curve: 73 | human_curve = HumanizeMouseTrajectory( 74 | [origin[0], origin[1]], 75 | [x, y], 76 | offset_boundary_x=offset_boundary_x, 77 | offset_boundary_y=offset_boundary_y, 78 | knots_count=knots_count, 79 | distortion_mean=distortion_mean, 80 | distortion_st_dev=distortion_st_dev, 81 | distortion_frequency=distortion_frequency, 82 | tween=tween, 83 | target_points=target_points, 84 | ) 85 | 86 | extra_numbers = [0, 0] 87 | total_offset = [0, 0] 88 | for point in human_curve.points: 89 | x_offset, y_offset = point[0] - origin[0], point[1] - origin[1] 90 | extra_numbers[0] += x_offset - int(x_offset) 91 | extra_numbers[1] += y_offset - int(y_offset) 92 | if (abs(extra_numbers[0]) > 1) and (abs(extra_numbers[1]) > 1): 93 | self.__action.move_by_offset( 94 | int(extra_numbers[0]), int(extra_numbers[1]) 95 | ) 96 | total_offset[0] += int(extra_numbers[0]) 97 | total_offset[1] += int(extra_numbers[1]) 98 | extra_numbers[0] = extra_numbers[0] - int(extra_numbers[0]) 99 | extra_numbers[1] = extra_numbers[1] - int(extra_numbers[1]) 100 | elif abs(extra_numbers[0]) > 1: 101 | self.__action.move_by_offset((int(extra_numbers[0])), 0) 102 | total_offset[0] += int(extra_numbers[0]) 103 | extra_numbers[0] = extra_numbers[0] - int(extra_numbers[0]) 104 | elif abs(extra_numbers[1]) > 1: 105 | self.__action.move_by_offset(0, int(extra_numbers[1])) 106 | total_offset[1] += int(extra_numbers[1]) 107 | extra_numbers[1] = extra_numbers[1] - int(extra_numbers[1]) 108 | origin[0], origin[1] = point[0], point[1] 109 | total_offset[0] += int(x_offset) 110 | total_offset[1] += int(y_offset) 111 | self.__action.move_by_offset(int(x_offset), int(y_offset)) 112 | 113 | total_offset[0] += int(extra_numbers[0]) 114 | total_offset[1] += int(extra_numbers[1]) 115 | self.__action.move_by_offset(int(extra_numbers[0]), int(extra_numbers[1])) 116 | try: 117 | self.__action.perform() 118 | except MoveTargetOutOfBoundsException: 119 | self.__action.move_to_element(element_or_pos) 120 | print( 121 | "MoveTargetOutOfBoundsException, Cursor Moved to Point, but without Human Trajectory!" 122 | ) 123 | 124 | self.origin_coordinate = [ 125 | pre_origin[0] + total_offset[0], 126 | pre_origin[1] + total_offset[1], 127 | ] 128 | 129 | return [pre_origin[0] + total_offset[0], pre_origin[1] + total_offset[1]] 130 | -------------------------------------------------------------------------------- /humancursor/web_cursor.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import random 3 | 4 | from selenium.webdriver.common.action_chains import ActionChains 5 | from selenium.webdriver.remote.webelement import WebElement 6 | 7 | from humancursor.utilities.web_adjuster import WebAdjuster 8 | 9 | 10 | class WebCursor: 11 | def __init__(self, driver): 12 | self.__driver = driver 13 | self.__action = ActionChains(self.__driver, duration=0) 14 | self.human = WebAdjuster(self.__driver) 15 | self.origin_coordinates = [0, 0] 16 | 17 | def move_to( 18 | self, 19 | element: WebElement or list, 20 | relative_position: list = None, 21 | absolute_offset: bool = False, 22 | origin_coordinates=None, 23 | steady=False 24 | ): 25 | """Moves to element or coordinates with human curve""" 26 | if not self.scroll_into_view_of_element(element): 27 | return False 28 | if origin_coordinates is None: 29 | origin_coordinates = self.origin_coordinates 30 | self.origin_coordinates = self.human.move_to( 31 | element, 32 | origin_coordinates=origin_coordinates, 33 | absolute_offset=absolute_offset, 34 | relative_position=relative_position, 35 | steady=steady 36 | ) 37 | return self.origin_coordinates 38 | 39 | def click_on( 40 | self, 41 | element: WebElement or list, 42 | number_of_clicks: int = 1, 43 | click_duration: float = 0, 44 | relative_position: list = None, 45 | absolute_offset: bool = False, 46 | origin_coordinates=None, 47 | steady=False 48 | ): 49 | """Moves to element or coordinates with human curve, and clicks on it a specified number of times, default is 1""" 50 | self.move_to( 51 | element, 52 | origin_coordinates=origin_coordinates, 53 | absolute_offset=absolute_offset, 54 | relative_position=relative_position, 55 | steady=steady 56 | ) 57 | self.click(number_of_clicks=number_of_clicks, click_duration=click_duration) 58 | return True 59 | 60 | def click(self, number_of_clicks: int = 1, click_duration: float = 0): 61 | """Performs the click action""" 62 | if click_duration: 63 | click_action = lambda: self.__action.click_and_hold().pause(click_duration).release().pause( 64 | random.randint(170, 280) / 1000) 65 | else: 66 | click_action = lambda: self.__action.click().pause(random.randint(170, 280) / 1000) 67 | for _ in range(number_of_clicks): 68 | click_action() 69 | self.__action.perform() 70 | return True 71 | 72 | def move_by_offset(self, x: int, y: int, steady=False): 73 | """Moves the cursor with human curve, by specified number of x and y pixels""" 74 | self.origin_coordinates = self.human.move_to([x, y], absolute_offset=True, steady=steady) 75 | return True 76 | 77 | def drag_and_drop( 78 | self, 79 | drag_from_element: WebElement or list, 80 | drag_to_element: WebElement or list, 81 | drag_from_relative_position: list = None, 82 | drag_to_relative_position: list = None, 83 | steady=False 84 | ): 85 | """Moves to element or coordinates, clicks and holds, dragging it to another element, with human curve""" 86 | if drag_from_relative_position is None: 87 | self.move_to(drag_from_element) 88 | else: 89 | self.move_to( 90 | drag_from_element, relative_position=drag_from_relative_position 91 | ) 92 | 93 | if drag_to_element is None: 94 | self.__action.click().perform() 95 | else: 96 | self.__action.click_and_hold().perform() 97 | if drag_to_relative_position is None: 98 | self.move_to(drag_to_element, steady=steady) 99 | else: 100 | self.move_to( 101 | drag_to_element, relative_position=drag_to_relative_position, steady=steady 102 | ) 103 | self.__action.release().perform() 104 | 105 | return True 106 | 107 | def control_scroll_bar( 108 | self, 109 | scroll_bar_element: WebElement, 110 | amount_by_percentage: list, 111 | orientation: str = "horizontal", 112 | steady=False 113 | ): 114 | """Adjusts any scroll bar on the webpage, by the amount you want in float number from 0 to 1 115 | representing percentage of fullness, orientation of the scroll bar must also be defined by user 116 | horizontal or vertical""" 117 | direction = True if orientation == "horizontal" else False 118 | 119 | self.move_to(scroll_bar_element) 120 | self.__action.click_and_hold().perform() 121 | # TODO: this needs rework, it will be more natural if it goes out of scroll bar, up or down randomly 122 | if direction: 123 | self.move_to( 124 | scroll_bar_element, 125 | relative_position=[amount_by_percentage, random.randint(0, 100) / 100], 126 | steady=steady 127 | ) 128 | else: 129 | self.move_to( 130 | scroll_bar_element, 131 | relative_position=[random.randint(0, 100) / 100, amount_by_percentage], 132 | steady=steady 133 | ) 134 | 135 | self.__action.release().perform() 136 | 137 | return True 138 | 139 | def scroll_into_view_of_element(self, element: WebElement): 140 | """Scrolls the element into viewport, if not already in it""" 141 | if isinstance(element, WebElement): 142 | is_in_viewport = self.__driver.execute_script( 143 | """ 144 | var element = arguments[0]; 145 | var rect = element.getBoundingClientRect(); 146 | return ( 147 | rect.top >= 0 && 148 | rect.left >= 0 && 149 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 150 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 151 | ); 152 | """, 153 | element, 154 | ) 155 | if not is_in_viewport: 156 | self.__driver.execute_script( 157 | "arguments[0].scrollIntoView({ behavior: 'smooth', block: 'center' });", 158 | element, 159 | ) 160 | sleep(random.uniform(0.8, 1.4)) 161 | return True 162 | elif isinstance(element, list): 163 | """User should input correct coordinates of x and y, cant take any action""" 164 | return True 165 | else: 166 | print("Incorrect Element or Coordinates values!") 167 | return False 168 | 169 | def show_cursor(self): 170 | self.__driver.execute_script(''' 171 | let dot; 172 | function displayRedDot() { 173 | // Get the cursor position 174 | const x = event.clientX; 175 | const y = event.clientY; 176 | 177 | if (!dot) { 178 | // Create a new div element for the red dot if it doesn't exist 179 | dot = document.createElement("div"); 180 | // Style the dot with CSS 181 | dot.style.position = "fixed"; 182 | dot.style.width = "5px"; 183 | dot.style.height = "5px"; 184 | dot.style.borderRadius = "50%"; 185 | dot.style.backgroundColor = "red"; 186 | // Add the dot to the page 187 | document.body.appendChild(dot); 188 | } 189 | 190 | // Update the dot's position 191 | dot.style.left = x + "px"; 192 | dot.style.top = y + "px"; 193 | } 194 | 195 | // Add event listener to update the dot's position on mousemove 196 | document.addEventListener("mousemove", displayRedDot);''') 197 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "HumanCursor" 7 | version = "1.1.5" 8 | authors = [ 9 | { name="Flori Batusha", email="floribatusha0@gmail.com" }, 10 | ] 11 | description = "Simulate Human Cursor Movement for Automated Scripts" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | dependencies = [ 15 | "selenium >= 4.9.0", 16 | "pyautogui >= 0.9.53", 17 | "numpy >= 1.24.3" 18 | ] 19 | classifiers = [ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ] 24 | 25 | [project.urls] 26 | "Homepage" = "https://github.com/riflosnake/HumanCursor" 27 | --------------------------------------------------------------------------------