Skip to content

Commit 7c10270

Browse files
committed
Add playwright async api support, cleanup.
1 parent 371d56f commit 7c10270

File tree

7 files changed

+195
-17
lines changed

7 files changed

+195
-17
lines changed

pyppeteer_ghost_cursor/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def createCursor(*args, **kwargs):
1717
"""
1818
)
1919
if "performRandomMoves" in kwargs:
20-
kwargs["perform_random_moves"] = kwargs["performRandomMoves"]
20+
kwargs["perform_random_moves"] = kwargs["performRandomMoves"]
2121
return create_cursor(*args, **kwargs)
2222

2323

pyppeteer_ghost_cursor/playwright/async_api/mouse_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import Path
44

55

6-
async def install_mouse_helper(page: Page) -> Coroutine[None, None, None]:
6+
async def install_mouse_helper(page: Page):
77
await page.add_init_script(
88
path=Path(__file__).parent.joinpath("../js/mouseHelper.js")
99
)
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import asyncio
2+
import logging
3+
import random
4+
from typing import Union, Coroutine, Optional, Dict, List
5+
from playwright.async_api import Page, ElementHandle, CDPSession
6+
7+
from pyppeteer_ghost_cursor.shared.math import (
8+
Vector,
9+
origin,
10+
overshoot,
11+
)
12+
from pyppeteer_ghost_cursor.shared.spoof import (
13+
path,
14+
should_overshoot,
15+
get_random_box_point,
16+
)
17+
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
class GhostCursor:
23+
def __init__(self, page: Page, start: Vector):
24+
self.page = page
25+
self.previous = start
26+
self.moving = False
27+
self.overshoot_spread = 10
28+
self.overshoot_radius = 120
29+
30+
async def get_cdp_session(self) -> Coroutine[None, None, CDPSession]:
31+
if not hasattr(self, "cdp_session"):
32+
self.cdp_session = await self.page.context.new_cdp_session(self.page)
33+
return self.cdp_session
34+
35+
async def get_random_page_point(self) -> Coroutine[None, None, Vector]:
36+
"""Get a random point on a browser window"""
37+
target_id = self.page.target._targetId
38+
window = (await self.get_cdp_session()).send(
39+
"Browser.getWindowForTarget", {"targetId": target_id}
40+
)
41+
return get_random_box_point(
42+
{
43+
"x": origin.x,
44+
"y": origin.y,
45+
"width": window["bounds"]["width"],
46+
"height": window["bounds"]["height"],
47+
}
48+
)
49+
50+
async def random_move(self):
51+
"""Start random mouse movements. Function recursively calls itself"""
52+
try:
53+
if not self.moving:
54+
rand = await self.get_random_page_point()
55+
await self.trace_path(path(self.previous, rand), True)
56+
self.previous = rand
57+
await asyncio.sleep(random.random() * 2)
58+
asyncio.ensure_future(
59+
self.random_move()
60+
) # fire and forget, recursive function
61+
except:
62+
logger.debug("Warning: stopping random mouse movements")
63+
64+
async def trace_path(self, vectors: List[Vector], abort_on_move: bool = False):
65+
"""Move the mouse over a number of vectors"""
66+
for v in vectors:
67+
try:
68+
# In case this is called from random mouse movements and the users wants to move the mouse, abort
69+
if abort_on_move and self.moving:
70+
return
71+
await self.page.mouse.move(v.x, v.y)
72+
self.previous = v
73+
except Exception as exc:
74+
# Exit function if the browser is no longer connected
75+
if not (await self.page.browser.is_connected()):
76+
return
77+
logger.debug("Warning: could not move mouse, error message: %s", exc)
78+
79+
def toggle_random_move(self, random_: bool):
80+
self.moving = not random_
81+
82+
async def click(
83+
self,
84+
selector: Optional[Union[str, ElementHandle]],
85+
padding_percentage: Optional[float] = None,
86+
wait_for_selector: Optional[float] = None,
87+
wait_for_click: Optional[float] = None,
88+
):
89+
self.toggle_random_move(False)
90+
if selector is not None:
91+
await self.move(selector, padding_percentage, wait_for_selector)
92+
self.toggle_random_move(False)
93+
94+
try:
95+
await self.page.mouse.down()
96+
if wait_for_click is not None:
97+
await asyncio.sleep(wait_for_click / 1000)
98+
await self.page.mouse.up()
99+
except Exception as exc:
100+
logger.debug("Warning: could not click mouse, error message: %s", exc)
101+
102+
await asyncio.sleep(random.random() * 2)
103+
self.toggle_random_move(True)
104+
105+
async def move(
106+
self,
107+
selector: Union[str, ElementHandle],
108+
padding_percentage: Optional[float] = None,
109+
wait_for_selector: Optional[float] = None,
110+
):
111+
self.toggle_random_move(False)
112+
elem = None
113+
if isinstance(selector, str):
114+
if wait_for_selector:
115+
await self.page.wait_for_selector(selector, timeout=wait_for_selector)
116+
elem = await self.page.query_selector(selector)
117+
if elem is None:
118+
raise Exception(
119+
'Could not find element with selector "${}", make sure you\'re waiting for the elements with "puppeteer.wait_for_selector"'.format(
120+
selector
121+
)
122+
)
123+
else: # ElementHandle
124+
elem = selector
125+
126+
# Make sure the object is in view
127+
await elem.scroll_into_view_if_needed()
128+
box = await elem.bounding_box()
129+
if box is None:
130+
raise Exception(
131+
"Could not find the dimensions of the element you're clicking on, this might be a bug?"
132+
)
133+
destination = get_random_box_point(box, padding_percentage)
134+
dimensions = {"height": box["height"], "width": box["width"]}
135+
overshooting = should_overshoot(self.previous, destination)
136+
to = (
137+
overshoot(destination, self.overshoot_radius)
138+
if overshooting
139+
else destination
140+
)
141+
await self.trace_path(path(self.previous, to))
142+
143+
if overshooting:
144+
bounding_box = {
145+
"height": dimensions["height"],
146+
"width": dimensions["width"],
147+
"x": destination.x,
148+
"y": destination.y,
149+
}
150+
correction = path(to, bounding_box, self.overshoot_spread)
151+
await self.trace_path(correction)
152+
self.previous = destination
153+
self.toggle_random_move(True)
154+
155+
async def move_to(self, destination: dict):
156+
destination_vector = Vector(destination["x"], destination["y"])
157+
self.toggle_random_move(False)
158+
await self.trace_path(path(self.previous, destination_vector))
159+
self.toggle_random_move(True)
160+
161+
162+
def create_cursor(
163+
page, start: Union[Vector, Dict] = origin, perform_random_moves: bool = False
164+
) -> GhostCursor:
165+
if isinstance(start, dict):
166+
start = Vector(**start)
167+
cursor = GhostCursor(page, start)
168+
if perform_random_moves:
169+
asyncio.ensure_future(cursor.random_move()) # fire and forget
170+
return cursor

pyppeteer_ghost_cursor/playwright/sync_api/spoof.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,26 @@
22
import logging
33
import time
44
import random
5-
from typing import Union, Coroutine, Optional, Dict, List
6-
from playwright.sync_api import Page, ElementHandle, CDPSession
5+
from typing import Union, Optional, Dict, List
6+
from playwright.sync_api import Page, ElementHandle
77

88
from pyppeteer_ghost_cursor.shared.math import (
99
Vector,
1010
origin,
1111
overshoot,
1212
)
13-
from pyppeteer_ghost_cursor.shared.spoof import path, should_overshoot, get_random_box_point
13+
from pyppeteer_ghost_cursor.shared.spoof import (
14+
path,
15+
should_overshoot,
16+
get_random_box_point,
17+
)
1418

1519

1620
logger = logging.getLogger(__name__)
1721

1822

1923
class GhostCursor:
20-
def __init__(self, page, start: Vector):
24+
def __init__(self, page: Page, start: Vector):
2125
self.page = page
2226
self.previous = start
2327
self.moving = False
@@ -40,7 +44,7 @@ def get_random_page_point(self) -> Vector:
4044
}
4145
)
4246

43-
async def random_move(self) -> None:
47+
async def random_move(self):
4448
"""Start random mouse movements. Function recursively calls itself"""
4549
try:
4650
if not self.moving:

pyppeteer_ghost_cursor/pyppeteer/mouse_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import Path
44

55

6-
async def install_mouse_helper(page: Page) -> Coroutine[None, None, None]:
6+
async def install_mouse_helper(page: Page):
77
js_text = Path(__file__).parent.joinpath("../js/mouseHelper.js").read_text()
88
await page.evaluateOnNewDocument(
99
"() => {"

pyppeteer_ghost_cursor/pyppeteer/spoof.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@
1818
origin,
1919
overshoot,
2020
)
21-
from pyppeteer_ghost_cursor.shared.spoof import path, should_overshoot, get_random_box_point
21+
from pyppeteer_ghost_cursor.shared.spoof import (
22+
path,
23+
should_overshoot,
24+
get_random_box_point,
25+
)
2226

2327

2428
logger = logging.getLogger(__name__)
@@ -90,14 +94,14 @@ async def get_element_box(
9094

9195

9296
class GhostCursor:
93-
def __init__(self, page, start: Vector):
97+
def __init__(self, page: Page, start: Vector):
9498
self.page = page
9599
self.previous = start
96100
self.moving = False
97101
self.overshoot_spread = 10
98102
self.overshoot_radius = 120
99103

100-
async def random_move(self) -> Coroutine[None, None, None]:
104+
async def random_move(self):
101105
"""Start random mouse movements. Function recursively calls itself"""
102106
try:
103107
if not self.moving:
@@ -111,9 +115,7 @@ async def random_move(self) -> Coroutine[None, None, None]:
111115
except:
112116
logger.debug("Warning: stopping random mouse movements")
113117

114-
async def trace_path(
115-
self, vectors: List[Vector], abort_on_move: bool = False
116-
) -> Coroutine[None, None, None]:
118+
async def trace_path(self, vectors: List[Vector], abort_on_move: bool = False):
117119
"""Move the mouse over a number of vectors"""
118120
for v in vectors:
119121
try:
@@ -128,7 +130,7 @@ async def trace_path(
128130
return
129131
logger.debug("Warning: could not move mouse, error message: %s", exc)
130132

131-
def toggle_random_move(self, random_: bool):
133+
def toggle_random_move(self, random_: bool) -> None:
132134
self.moving = not random_
133135

134136
async def click(
@@ -218,7 +220,7 @@ async def move(
218220
self.previous = destination
219221
self.toggle_random_move(True)
220222

221-
async def moveTo(self, destination: dict) -> Coroutine[None, None, None]:
223+
async def moveTo(self, destination: dict):
222224
destination_vector = Vector(destination["x"], destination["y"])
223225
self.toggle_random_move(False)
224226
await self.trace_path(path(self.previous, destination_vector))

pyppeteer_ghost_cursor/shared/spoof.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def get_path(start: Dict, end: Dict) -> List[Dict]:
5656
return [el.__dict__ for el in vectors]
5757

5858

59-
def get_random_box_point(box: Dict, padding_percentage: Optional[float] = None) -> Vector:
59+
def get_random_box_point(
60+
box: Dict, padding_percentage: Optional[float] = None
61+
) -> Vector:
6062
"""Get a random point on a box"""
6163
paddingWidth = paddingHeight = 0
6264
if (

0 commit comments

Comments
 (0)