nomix-clicker 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nomix_clicker-0.1.0/LICENSE +21 -0
- nomix_clicker-0.1.0/PKG-INFO +111 -0
- nomix_clicker-0.1.0/README.md +84 -0
- nomix_clicker-0.1.0/pyproject.toml +38 -0
- nomix_clicker-0.1.0/setup.cfg +4 -0
- nomix_clicker-0.1.0/src/nomix_clicker/__init__.py +52 -0
- nomix_clicker-0.1.0/src/nomix_clicker/actions.py +184 -0
- nomix_clicker-0.1.0/src/nomix_clicker/agent.py +78 -0
- nomix_clicker-0.1.0/src/nomix_clicker/api_helper.py +235 -0
- nomix_clicker-0.1.0/src/nomix_clicker/clicker.py +91 -0
- nomix_clicker-0.1.0/src/nomix_clicker/config_handler.py +35 -0
- nomix_clicker-0.1.0/src/nomix_clicker/environment.py +30 -0
- nomix_clicker-0.1.0/src/nomix_clicker/recognition.py +122 -0
- nomix_clicker-0.1.0/src/nomix_clicker.egg-info/PKG-INFO +111 -0
- nomix_clicker-0.1.0/src/nomix_clicker.egg-info/SOURCES.txt +21 -0
- nomix_clicker-0.1.0/src/nomix_clicker.egg-info/dependency_links.txt +1 -0
- nomix_clicker-0.1.0/src/nomix_clicker.egg-info/requires.txt +5 -0
- nomix_clicker-0.1.0/src/nomix_clicker.egg-info/top_level.txt +1 -0
- nomix_clicker-0.1.0/tests/test_actions.py +42 -0
- nomix_clicker-0.1.0/tests/test_clicker.py +49 -0
- nomix_clicker-0.1.0/tests/test_config.py +57 -0
- nomix_clicker-0.1.0/tests/test_package.py +28 -0
- nomix_clicker-0.1.0/tests/test_recognition.py +79 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Timofey Ivanenko
|
|
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.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nomix-clicker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: iOS device automation via the Nomix Clicker API — simulated touch input with AI screen recognition.
|
|
5
|
+
Author: Timofey Ivanenko
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://panel.nomixclicker.com
|
|
8
|
+
Project-URL: Documentation, https://panel.nomixclicker.com/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/timofeyivanenko/ClickerScriptingLibrary
|
|
10
|
+
Keywords: automation,ios,clicker,nomix,ui-automation,device-automation
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: requests>=2.32
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
## NomixClicker Scripting Library
|
|
29
|
+
|
|
30
|
+
This is a library of ready-made scripts written in Python and using the free [Clicker API](https://panel.nomixclicker.com/docs).
|
|
31
|
+
|
|
32
|
+
### What Is Clicker?
|
|
33
|
+
|
|
34
|
+
NomixClicker is a controlling dongle for iOS. It allows manual control, automatic control via API, and fully autonomous AI mode.
|
|
35
|
+
|
|
36
|
+
Part of the features are work-in-progress, see the [Roadmap](https://nomixclicker.com/).
|
|
37
|
+
|
|
38
|
+
<p align="center">
|
|
39
|
+
<img src="res/reddit-script-example.gif" width="480" height="360">
|
|
40
|
+
<br>
|
|
41
|
+
<em style="font-size: 0.85em; opacity: 0.7;">Example: quick script for Reddit warmup.</em>
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
### Installation
|
|
45
|
+
|
|
46
|
+
Requires Python 3.10+. Install from PyPI:
|
|
47
|
+
|
|
48
|
+
```commandline
|
|
49
|
+
pip install nomix-clicker
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Buy a Clicker device if you don't have it (delivery takes ~2 weeks to any place in the world), then open the [Panel](https://panel.nomixclicker.com/choose_device) and get your API token.
|
|
53
|
+
|
|
54
|
+
[Purchase on the official site only.](https://panel.nomixclicker.com/payment)
|
|
55
|
+
|
|
56
|
+
### Configuration
|
|
57
|
+
|
|
58
|
+
Provide your API token and device id in either of two ways (environment variables take precedence):
|
|
59
|
+
|
|
60
|
+
**Environment variables:**
|
|
61
|
+
|
|
62
|
+
```commandline
|
|
63
|
+
export NOMIX_API_KEY="your-api-key"
|
|
64
|
+
export NOMIX_DEVICE_ID="your-device-id"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Or a `config.json`** in your working directory (copy `config.example.json`):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"API_KEY": "your-api-key",
|
|
72
|
+
"DEVICE_ID": "your-device-id",
|
|
73
|
+
"API_URL": "https://panel.nomixclicker.com/clicker/v1"
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Quick start
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
from nomix_clicker import Clicker, parse_screen, open_app, DEVICE_ID
|
|
81
|
+
|
|
82
|
+
clicker = Clicker(DEVICE_ID)
|
|
83
|
+
open_app(clicker, "Instagram")
|
|
84
|
+
|
|
85
|
+
screen = parse_screen(clicker)
|
|
86
|
+
screen.find_and_click(clicker, "like")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Manual control of iPhones will work out-of-the-box, no setup needed.
|
|
90
|
+
|
|
91
|
+
### Examples
|
|
92
|
+
|
|
93
|
+
Ready-made example scripts live in the [`examples/`](examples/) folder. After installing the package, run one directly, for example:
|
|
94
|
+
|
|
95
|
+
```commandline
|
|
96
|
+
python3 examples/airplane-mode-iphone-12.py
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- [**instagram-warmup.py**](examples/instagram-warmup.py) - Scrolling Instagram Reels, liking, and commenting with configurable probabilities.
|
|
100
|
+
- [**restart_all_devices.py**](examples/restart_all_devices.py) - Restarts all devices associated with your account.
|
|
101
|
+
- [**airplane-mode-iphone-12.py**](examples/airplane-mode-iphone-12.py) - Toggles airplane mode on and off on iPhone 12.
|
|
102
|
+
- [**switch-screen-viewing-iphone-12.py**](examples/switch-screen-viewing-iphone-12.py) - Turns on/off Clicker screen viewing.
|
|
103
|
+
|
|
104
|
+
### Local development
|
|
105
|
+
|
|
106
|
+
To work on the library itself, install it in editable mode:
|
|
107
|
+
|
|
108
|
+
```commandline
|
|
109
|
+
pip install -e .
|
|
110
|
+
```
|
|
111
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
## NomixClicker Scripting Library
|
|
2
|
+
|
|
3
|
+
This is a library of ready-made scripts written in Python and using the free [Clicker API](https://panel.nomixclicker.com/docs).
|
|
4
|
+
|
|
5
|
+
### What Is Clicker?
|
|
6
|
+
|
|
7
|
+
NomixClicker is a controlling dongle for iOS. It allows manual control, automatic control via API, and fully autonomous AI mode.
|
|
8
|
+
|
|
9
|
+
Part of the features are work-in-progress, see the [Roadmap](https://nomixclicker.com/).
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="res/reddit-script-example.gif" width="480" height="360">
|
|
13
|
+
<br>
|
|
14
|
+
<em style="font-size: 0.85em; opacity: 0.7;">Example: quick script for Reddit warmup.</em>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
|
|
19
|
+
Requires Python 3.10+. Install from PyPI:
|
|
20
|
+
|
|
21
|
+
```commandline
|
|
22
|
+
pip install nomix-clicker
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Buy a Clicker device if you don't have it (delivery takes ~2 weeks to any place in the world), then open the [Panel](https://panel.nomixclicker.com/choose_device) and get your API token.
|
|
26
|
+
|
|
27
|
+
[Purchase on the official site only.](https://panel.nomixclicker.com/payment)
|
|
28
|
+
|
|
29
|
+
### Configuration
|
|
30
|
+
|
|
31
|
+
Provide your API token and device id in either of two ways (environment variables take precedence):
|
|
32
|
+
|
|
33
|
+
**Environment variables:**
|
|
34
|
+
|
|
35
|
+
```commandline
|
|
36
|
+
export NOMIX_API_KEY="your-api-key"
|
|
37
|
+
export NOMIX_DEVICE_ID="your-device-id"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Or a `config.json`** in your working directory (copy `config.example.json`):
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"API_KEY": "your-api-key",
|
|
45
|
+
"DEVICE_ID": "your-device-id",
|
|
46
|
+
"API_URL": "https://panel.nomixclicker.com/clicker/v1"
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Quick start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from nomix_clicker import Clicker, parse_screen, open_app, DEVICE_ID
|
|
54
|
+
|
|
55
|
+
clicker = Clicker(DEVICE_ID)
|
|
56
|
+
open_app(clicker, "Instagram")
|
|
57
|
+
|
|
58
|
+
screen = parse_screen(clicker)
|
|
59
|
+
screen.find_and_click(clicker, "like")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Manual control of iPhones will work out-of-the-box, no setup needed.
|
|
63
|
+
|
|
64
|
+
### Examples
|
|
65
|
+
|
|
66
|
+
Ready-made example scripts live in the [`examples/`](examples/) folder. After installing the package, run one directly, for example:
|
|
67
|
+
|
|
68
|
+
```commandline
|
|
69
|
+
python3 examples/airplane-mode-iphone-12.py
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- [**instagram-warmup.py**](examples/instagram-warmup.py) - Scrolling Instagram Reels, liking, and commenting with configurable probabilities.
|
|
73
|
+
- [**restart_all_devices.py**](examples/restart_all_devices.py) - Restarts all devices associated with your account.
|
|
74
|
+
- [**airplane-mode-iphone-12.py**](examples/airplane-mode-iphone-12.py) - Toggles airplane mode on and off on iPhone 12.
|
|
75
|
+
- [**switch-screen-viewing-iphone-12.py**](examples/switch-screen-viewing-iphone-12.py) - Turns on/off Clicker screen viewing.
|
|
76
|
+
|
|
77
|
+
### Local development
|
|
78
|
+
|
|
79
|
+
To work on the library itself, install it in editable mode:
|
|
80
|
+
|
|
81
|
+
```commandline
|
|
82
|
+
pip install -e .
|
|
83
|
+
```
|
|
84
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nomix-clicker"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "iOS device automation via the Nomix Clicker API — simulated touch input with AI screen recognition."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Timofey Ivanenko" }]
|
|
13
|
+
keywords = ["automation", "ios", "clicker", "nomix", "ui-automation", "device-automation"]
|
|
14
|
+
dependencies = ["requests>=2.32"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
dev = ["pytest>=8.0", "build>=1.0"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://panel.nomixclicker.com"
|
|
31
|
+
Documentation = "https://panel.nomixclicker.com/docs"
|
|
32
|
+
Repository = "https://github.com/timofeyivanenko/ClickerScriptingLibrary"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""nomix_clicker — iOS device automation via the Nomix Clicker API.
|
|
2
|
+
|
|
3
|
+
Simulate touch input and use AI screen recognition to navigate apps.
|
|
4
|
+
|
|
5
|
+
from nomix_clicker import Clicker, parse_screen, open_app
|
|
6
|
+
|
|
7
|
+
clicker = Clicker("your-device-id")
|
|
8
|
+
open_app(clicker, "Instagram")
|
|
9
|
+
screen = parse_screen(clicker)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .actions import (
|
|
13
|
+
chance_tap,
|
|
14
|
+
close_app,
|
|
15
|
+
find_and_click,
|
|
16
|
+
is_ad,
|
|
17
|
+
open_app,
|
|
18
|
+
post_comment,
|
|
19
|
+
random_sleep,
|
|
20
|
+
swipe_back,
|
|
21
|
+
swipe_feed,
|
|
22
|
+
)
|
|
23
|
+
from .agent import Agent
|
|
24
|
+
from .clicker import Clicker
|
|
25
|
+
from .environment import API_KEY, API_URL, DEVICE_ID
|
|
26
|
+
from .recognition import Element, Screen, parse_screen
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
# core
|
|
32
|
+
"Clicker",
|
|
33
|
+
"Agent",
|
|
34
|
+
"Screen",
|
|
35
|
+
"Element",
|
|
36
|
+
"parse_screen",
|
|
37
|
+
# actions
|
|
38
|
+
"open_app",
|
|
39
|
+
"close_app",
|
|
40
|
+
"find_and_click",
|
|
41
|
+
"swipe_feed",
|
|
42
|
+
"swipe_back",
|
|
43
|
+
"is_ad",
|
|
44
|
+
"chance_tap",
|
|
45
|
+
"post_comment",
|
|
46
|
+
"random_sleep",
|
|
47
|
+
# config
|
|
48
|
+
"API_URL",
|
|
49
|
+
"API_KEY",
|
|
50
|
+
"DEVICE_ID",
|
|
51
|
+
"__version__",
|
|
52
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from time import sleep
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .clicker import Clicker
|
|
6
|
+
from .recognition import Screen, parse_screen
|
|
7
|
+
|
|
8
|
+
COMMENT_SECTION_KEYWORDS = [
|
|
9
|
+
"close comments",
|
|
10
|
+
"filter comments",
|
|
11
|
+
"add comment",
|
|
12
|
+
"add a comment",
|
|
13
|
+
"join the conversation",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def open_app(clicker: Clicker, app_name: str, retries: int = 3) -> Optional[Screen]:
|
|
18
|
+
"""Open any app via iOS Spotlight search. Retries up to `retries` times.
|
|
19
|
+
|
|
20
|
+
Returns the parsed screen on success, or None on failure.
|
|
21
|
+
"""
|
|
22
|
+
for attempt in range(1, retries + 1):
|
|
23
|
+
print(f"Opening Spotlight (attempt {attempt}/{retries})...")
|
|
24
|
+
clicker.swipe((16000, 10000), down=8000, duration=300)
|
|
25
|
+
sleep(0.5)
|
|
26
|
+
|
|
27
|
+
print(f"Typing '{app_name}'...")
|
|
28
|
+
clicker.type(app_name)
|
|
29
|
+
sleep(2)
|
|
30
|
+
|
|
31
|
+
screen = parse_screen(clicker)
|
|
32
|
+
if not screen or not screen.find_and_click(clicker, app_name):
|
|
33
|
+
print(f"WARNING: '{app_name}' not found in Spotlight results")
|
|
34
|
+
close_app(clicker)
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
sleep(3)
|
|
38
|
+
|
|
39
|
+
screen = parse_screen(clicker)
|
|
40
|
+
if screen and app_name.lower() in screen.app_name.lower():
|
|
41
|
+
return screen
|
|
42
|
+
|
|
43
|
+
print(f"WARNING: '{app_name}' did not open (current app: '{screen.app_name if screen else 'None'}')")
|
|
44
|
+
close_app(clicker)
|
|
45
|
+
|
|
46
|
+
print(f"ERROR: Failed to open '{app_name}' after {retries} attempts.")
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _do_close_app(clicker: Clicker) -> None:
|
|
51
|
+
# Slow swipe up from bottom edge to open app switcher
|
|
52
|
+
sleep(1)
|
|
53
|
+
clicker.swipe((16384, 32767), up=4767, duration=1000)
|
|
54
|
+
sleep(5)
|
|
55
|
+
# Swipe up on the last app card to dismiss it
|
|
56
|
+
clicker.swipe((26500, 20000), up=10000, duration=300)
|
|
57
|
+
sleep(5)
|
|
58
|
+
# Tap home area to exit app switcher
|
|
59
|
+
clicker.click((16384, 30000), duration=100)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def close_app(clicker: Clicker, retries: int = 3) -> bool:
|
|
63
|
+
"""Open app switcher, dismiss the last app, then tap home.
|
|
64
|
+
|
|
65
|
+
Verifies the device returned to the Home Screen after closing.
|
|
66
|
+
Retries up to `retries` times if the app is still in the foreground.
|
|
67
|
+
Returns True if the Home Screen is confirmed, False otherwise.
|
|
68
|
+
"""
|
|
69
|
+
for attempt in range(1, retries + 1):
|
|
70
|
+
print(f"Closing app (attempt {attempt}/{retries})...")
|
|
71
|
+
_do_close_app(clicker)
|
|
72
|
+
sleep(3)
|
|
73
|
+
screen = parse_screen(clicker)
|
|
74
|
+
if not screen or screen.app_name.lower() == "home screen":
|
|
75
|
+
print("App closed successfully.")
|
|
76
|
+
return True
|
|
77
|
+
print(f"App still open after attempt {attempt} (screen: '{screen.app_name}'), retrying...")
|
|
78
|
+
print(f"Warning: could not confirm app was closed after {retries} attempts.")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_AD_KEYWORDS = [
|
|
83
|
+
"advertising", "advertisement", "sponsored",
|
|
84
|
+
"contact us", "shop now", "learn more",
|
|
85
|
+
"install now", "send message", "get quote",
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_ad(screen: Screen) -> bool:
|
|
90
|
+
"""Return True if the current screen looks like an ad."""
|
|
91
|
+
return (screen.contains(*_AD_KEYWORDS)
|
|
92
|
+
or any(el.content.lower() == "ad" for el in screen.elements))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def is_comment_section_opened(screen: Screen) -> bool:
|
|
96
|
+
"""Return True if the comments sheet is currently pulled up."""
|
|
97
|
+
return screen.contains(*COMMENT_SECTION_KEYWORDS)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def chance_tap(clicker: Clicker, screen: Screen, name: str, chance: float) -> bool:
|
|
101
|
+
"""Roll the dice and tap a button found on screen. Returns True if tapped."""
|
|
102
|
+
if random.random() >= chance:
|
|
103
|
+
return False
|
|
104
|
+
return screen.find_and_click(clicker, name)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def random_sleep(min_s: float = 0.3, max_s: float = 0.8) -> None:
|
|
108
|
+
"""Sleep for a random duration to simulate human behavior."""
|
|
109
|
+
sleep(random.uniform(min_s, max_s))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def swipe_back(clicker: Clicker) -> None:
|
|
113
|
+
"""iOS swipe-back gesture (swipe right from left edge)."""
|
|
114
|
+
clicker.swipe((5000, 16000), right=20000, duration=300)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def swipe_feed(clicker: Clicker) -> None:
|
|
118
|
+
"""Swipe up to the next feed item."""
|
|
119
|
+
clicker.swipe((16383, 26213), up=6553, duration=100)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def find_and_click(clicker: Clicker, *keywords: str, interactive_only: bool = True) -> bool:
|
|
123
|
+
"""Get screen, find element by keywords, and click it. Returns True if clicked."""
|
|
124
|
+
screen = parse_screen(clicker)
|
|
125
|
+
if not screen:
|
|
126
|
+
return False
|
|
127
|
+
return screen.find_and_click(clicker, *keywords, interactive_only=interactive_only)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def post_comment(
|
|
131
|
+
clicker: Clicker,
|
|
132
|
+
text: str,
|
|
133
|
+
input_keywords: list[str],
|
|
134
|
+
submit_keyword: str | list[str],
|
|
135
|
+
cached_coords: Optional[dict] = None,
|
|
136
|
+
) -> bool:
|
|
137
|
+
"""Find a comment input field, type text, and submit.
|
|
138
|
+
|
|
139
|
+
On the first call, parses the screen twice: once to find the input field,
|
|
140
|
+
and again after typing to find the send button (which only appears once the
|
|
141
|
+
keyboard is open). Coords are stored in `cached_coords` so subsequent calls
|
|
142
|
+
skip both parses entirely.
|
|
143
|
+
"""
|
|
144
|
+
if cached_coords and "comment_input" in cached_coords and "comment_submit" in cached_coords:
|
|
145
|
+
clicker.click(cached_coords["comment_input"])
|
|
146
|
+
clicker.type(text)
|
|
147
|
+
sleep(0.5)
|
|
148
|
+
clicker.click(cached_coords["comment_submit"])
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
screen = parse_screen(clicker)
|
|
152
|
+
if not screen:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
input_coords = screen.find(*input_keywords, interactive_only=False)
|
|
156
|
+
if not input_coords:
|
|
157
|
+
print("WARNING: Comment input not found")
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
clicker.click(input_coords)
|
|
161
|
+
clicker.type(text)
|
|
162
|
+
sleep(1)
|
|
163
|
+
|
|
164
|
+
screen = parse_screen(clicker)
|
|
165
|
+
if not screen:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
keywords = submit_keyword if isinstance(submit_keyword, list) else [submit_keyword]
|
|
169
|
+
submit_coords = screen.find(*keywords, interactive_only=False)
|
|
170
|
+
if not submit_coords:
|
|
171
|
+
print(f"WARNING: Submit button not found, elements: {[e.content for e in screen.elements]}")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
if cached_coords is not None:
|
|
175
|
+
cached_coords["comment_input"] = input_coords
|
|
176
|
+
cached_coords["comment_submit"] = submit_coords
|
|
177
|
+
print(f"Cached comment_input at {input_coords}, comment_submit at {submit_coords}")
|
|
178
|
+
|
|
179
|
+
clicker.click(submit_coords)
|
|
180
|
+
sleep(1)
|
|
181
|
+
clicker.click((16383, 4096)) # dismiss comments sheet
|
|
182
|
+
sleep(0.5)
|
|
183
|
+
|
|
184
|
+
return True
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from .api_helper import run_agent, get_agent_task, cancel_agent_task
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Agent:
|
|
7
|
+
"""AI agent controller for a device."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, device_id: str):
|
|
10
|
+
self.device_id = device_id
|
|
11
|
+
self.current_task_id: str | None = None
|
|
12
|
+
|
|
13
|
+
def execute(self, task: str) -> str:
|
|
14
|
+
"""Start a new agent task. Returns task_id and stores it internally."""
|
|
15
|
+
result = run_agent(self.device_id, task)
|
|
16
|
+
task_id = result.get("task_id")
|
|
17
|
+
if not task_id:
|
|
18
|
+
raise ValueError(f"API response missing 'task_id': {result}")
|
|
19
|
+
self.current_task_id = task_id
|
|
20
|
+
return self.current_task_id
|
|
21
|
+
|
|
22
|
+
def get_status(self, task_id: str | None = None) -> dict:
|
|
23
|
+
"""Get task status. Uses current_task_id if none provided."""
|
|
24
|
+
tid = task_id or self.current_task_id
|
|
25
|
+
if not tid:
|
|
26
|
+
raise ValueError("No task_id -- call execute() first or pass task_id")
|
|
27
|
+
return get_agent_task(self.device_id, tid)
|
|
28
|
+
|
|
29
|
+
def cancel(self, task_id: str | None = None) -> dict:
|
|
30
|
+
"""Cancel a running task."""
|
|
31
|
+
tid = task_id or self.current_task_id
|
|
32
|
+
if not tid:
|
|
33
|
+
raise ValueError("No task_id -- call execute() first or pass task_id")
|
|
34
|
+
result = cancel_agent_task(self.device_id, tid)
|
|
35
|
+
if tid == self.current_task_id:
|
|
36
|
+
self.current_task_id = None
|
|
37
|
+
return result
|
|
38
|
+
|
|
39
|
+
def poll(
|
|
40
|
+
self,
|
|
41
|
+
task_id: str | None = None,
|
|
42
|
+
interval: float = 1.0,
|
|
43
|
+
) -> str | None:
|
|
44
|
+
"""Poll task until completion, printing events to stdout."""
|
|
45
|
+
tid = task_id or self.current_task_id
|
|
46
|
+
if not tid:
|
|
47
|
+
raise ValueError("No task_id -- call execute() first or pass task_id")
|
|
48
|
+
|
|
49
|
+
seen_events = 0
|
|
50
|
+
while True:
|
|
51
|
+
task = get_agent_task(self.device_id, tid)
|
|
52
|
+
events = task.get("events", [])
|
|
53
|
+
|
|
54
|
+
for event in events[seen_events:]:
|
|
55
|
+
step = event.get("step", "?")
|
|
56
|
+
max_steps = event.get("max_steps", "?")
|
|
57
|
+
action = event.get("action", "?")
|
|
58
|
+
result = event.get("result", "?")
|
|
59
|
+
print(f"[{step}/{max_steps}] {action} -> {result}")
|
|
60
|
+
seen_events = len(events)
|
|
61
|
+
|
|
62
|
+
status = task.get("status")
|
|
63
|
+
if status not in ("pending", "running"):
|
|
64
|
+
result = task.get("result")
|
|
65
|
+
print(f"\n--- Agent {status}: {result}")
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
time.sleep(interval)
|
|
69
|
+
|
|
70
|
+
def run(self, task: str) -> str | None:
|
|
71
|
+
"""Convenience: execute a task and poll until done."""
|
|
72
|
+
try:
|
|
73
|
+
self.execute(task)
|
|
74
|
+
return self.poll()
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
print("\n\nInterrupted — cancelling task...")
|
|
77
|
+
self.cancel()
|
|
78
|
+
return
|