├── 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 |
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 |
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 |
--------------------------------------------------------------------------------