web3-agent-kit 0.8.0__tar.gz → 0.9.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.
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/PKG-INFO +1 -1
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/pyproject.toml +1 -1
- web3_agent_kit-0.9.0/src/airdrop/executor/__init__.py +33 -0
- web3_agent_kit-0.9.0/src/airdrop/executor/browser.py +435 -0
- web3_agent_kit-0.9.0/src/airdrop/executor/cli.py +402 -0
- web3_agent_kit-0.9.0/src/airdrop/executor/gleam_exec.py +664 -0
- web3_agent_kit-0.9.0/src/airdrop/executor/social_exec.py +789 -0
- web3_agent_kit-0.9.0/src/airdrop/executor/zealy_exec.py +535 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/__init__.py +3 -3
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_api.py +1 -1
- web3_agent_kit-0.9.0/tests/test_executor.py +250 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/PKG-INFO +1 -1
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/SOURCES.txt +7 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/LICENSE +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/README.md +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/setup.cfg +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/setup.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/__init__.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/agent.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/__init__.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/base.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/galxe.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/gleam.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/multi_wallet.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/social.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/tracker.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/airdrop/zealy.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/models.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/__init__.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/approval.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/bridge.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/dca.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/gas.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/portfolio.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/swap.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/wallet.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/watcher.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/api/routes/yield_opt.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/approval_manager.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/bridge.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/chain.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/dca_bot.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/defi/__init__.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/gas_optimizer.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/llm.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/multi_wallet.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/plugins/__init__.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/plugins/examples/gas_tracker.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/portfolio.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/sniper.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/utils/__init__.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/wallet.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/wallet_watcher.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/src/yield_optimizer.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_airdrop.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_bridge.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_core.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_dca_bot.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_defi.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_llm.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_multi_wallet.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_new_features.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_plugins.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_portfolio.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_sniper.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/tests/test_yield_optimizer.py +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/dependency_links.txt +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/requires.txt +0 -0
- {web3_agent_kit-0.8.0 → web3_agent_kit-0.9.0}/web3_agent_kit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "web3-agent-kit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "Open-source framework for building autonomous AI agents that interact with blockchain networks"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Executor layer — browser automation for airdrop task completion.
|
|
2
|
+
|
|
3
|
+
Provides real browser-based automation for Gleam.io, Zealy, and social
|
|
4
|
+
platform tasks using Playwright with anti-detect capabilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .browser import BrowserManager, BrowserConfig
|
|
8
|
+
from .gleam_exec import GleamExecutor, GleamResult, GleamTaskEntry
|
|
9
|
+
from .social_exec import (
|
|
10
|
+
TwitterExecutor,
|
|
11
|
+
DiscordExecutor,
|
|
12
|
+
TelegramExecutor,
|
|
13
|
+
SocialExecutorConfig,
|
|
14
|
+
)
|
|
15
|
+
from .zealy_exec import ZealyExecutor, ZealyResult
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Browser
|
|
19
|
+
"BrowserManager",
|
|
20
|
+
"BrowserConfig",
|
|
21
|
+
# Gleam
|
|
22
|
+
"GleamExecutor",
|
|
23
|
+
"GleamResult",
|
|
24
|
+
"GleamTaskEntry",
|
|
25
|
+
# Social
|
|
26
|
+
"TwitterExecutor",
|
|
27
|
+
"DiscordExecutor",
|
|
28
|
+
"TelegramExecutor",
|
|
29
|
+
"SocialExecutorConfig",
|
|
30
|
+
# Zealy
|
|
31
|
+
"ZealyExecutor",
|
|
32
|
+
"ZealyResult",
|
|
33
|
+
]
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
"""Anti-detect browser management using Playwright.
|
|
2
|
+
|
|
3
|
+
Provides stealth browsing with viewport randomization, user-agent rotation,
|
|
4
|
+
timezone spoofing, cookie persistence, and proxy support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import random
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Graceful import — playwright is optional
|
|
21
|
+
try:
|
|
22
|
+
from playwright.sync_api import (
|
|
23
|
+
Browser,
|
|
24
|
+
BrowserContext,
|
|
25
|
+
Page,
|
|
26
|
+
Playwright,
|
|
27
|
+
sync_playwright,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
HAS_PLAYWRIGHT = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_PLAYWRIGHT = False
|
|
33
|
+
# Type stubs for when playwright isn't installed
|
|
34
|
+
Browser = Any # type: ignore[misc,assignment]
|
|
35
|
+
BrowserContext = Any # type: ignore[misc,assignment]
|
|
36
|
+
Page = Any # type: ignore[misc,assignment]
|
|
37
|
+
Playwright = Any # type: ignore[misc,assignment]
|
|
38
|
+
|
|
39
|
+
SESSIONS_DIR = Path.home() / ".web3-agent-kit" / "sessions"
|
|
40
|
+
|
|
41
|
+
USER_AGENTS = [
|
|
42
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
43
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
|
|
44
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
45
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
|
|
46
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
|
47
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
48
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
TIMEZONES = [
|
|
52
|
+
"America/New_York",
|
|
53
|
+
"America/Chicago",
|
|
54
|
+
"America/Los_Angeles",
|
|
55
|
+
"Europe/London",
|
|
56
|
+
"Europe/Berlin",
|
|
57
|
+
"Asia/Singapore",
|
|
58
|
+
"Asia/Tokyo",
|
|
59
|
+
"Australia/Sydney",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
VIEWPORTS = [
|
|
63
|
+
{"width": 1920, "height": 1080},
|
|
64
|
+
{"width": 1366, "height": 768},
|
|
65
|
+
{"width": 1536, "height": 864},
|
|
66
|
+
{"width": 1440, "height": 900},
|
|
67
|
+
{"width": 1280, "height": 720},
|
|
68
|
+
{"width": 2560, "height": 1440},
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class BrowserConfig:
|
|
74
|
+
"""Configuration for the anti-detect browser."""
|
|
75
|
+
|
|
76
|
+
headless: bool = True
|
|
77
|
+
proxy: Optional[str] = None # http://host:port or socks5://host:port
|
|
78
|
+
user_agent: Optional[str] = None # None = random
|
|
79
|
+
timezone: Optional[str] = None # None = random
|
|
80
|
+
viewport: Optional[dict[str, int]] = None # None = random
|
|
81
|
+
session_name: str = "default"
|
|
82
|
+
sessions_dir: Path = SESSIONS_DIR
|
|
83
|
+
screenshot_dir: Optional[Path] = None
|
|
84
|
+
max_retries: int = 3
|
|
85
|
+
retry_delay: float = 2.0
|
|
86
|
+
slow_mo: int = 0 # ms delay between actions
|
|
87
|
+
extra_args: list[str] = field(default_factory=list)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BrowserManager:
|
|
91
|
+
"""Anti-detect browser manager using Playwright.
|
|
92
|
+
|
|
93
|
+
Manages browser lifecycle with stealth features: viewport randomization,
|
|
94
|
+
user-agent rotation, timezone spoofing, cookie persistence, and proxy support.
|
|
95
|
+
|
|
96
|
+
Example::
|
|
97
|
+
|
|
98
|
+
config = BrowserConfig(headless=True, proxy="socks5://proxy:1080")
|
|
99
|
+
manager = BrowserManager(config)
|
|
100
|
+
|
|
101
|
+
manager.launch()
|
|
102
|
+
page = manager.new_page()
|
|
103
|
+
page.goto("https://gleam.io/contest/abc123")
|
|
104
|
+
manager.screenshot(page, "contest.png")
|
|
105
|
+
manager.close()
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, config: Optional[BrowserConfig] = None):
|
|
109
|
+
if not HAS_PLAYWRIGHT:
|
|
110
|
+
raise ImportError(
|
|
111
|
+
"playwright is required for browser automation. "
|
|
112
|
+
"Install with: pip install playwright && playwright install chromium"
|
|
113
|
+
)
|
|
114
|
+
self.config = config or BrowserConfig()
|
|
115
|
+
self._playwright: Optional[Playwright] = None
|
|
116
|
+
self._browser: Optional[Browser] = None
|
|
117
|
+
self._context: Optional[BrowserContext] = None
|
|
118
|
+
self._pages: list[Page] = []
|
|
119
|
+
self._session_dir = self.config.sessions_dir / self.config.session_name
|
|
120
|
+
self._session_dir.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
def launch(self) -> None:
|
|
123
|
+
"""Launch the browser with stealth configuration.
|
|
124
|
+
|
|
125
|
+
Sets up anti-detect measures including randomized viewport,
|
|
126
|
+
user-agent, timezone, and proxy if configured.
|
|
127
|
+
"""
|
|
128
|
+
self._playwright = sync_playwright().start()
|
|
129
|
+
|
|
130
|
+
# Build launch args with stealth flags
|
|
131
|
+
launch_args = [
|
|
132
|
+
"--disable-blink-features=AutomationControlled",
|
|
133
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
134
|
+
"--disable-site-isolation-trials",
|
|
135
|
+
"--no-sandbox",
|
|
136
|
+
"--disable-setuid-sandbox",
|
|
137
|
+
"--disable-dev-shm-usage",
|
|
138
|
+
"--disable-accelerated-2d-canvas",
|
|
139
|
+
"--no-first-run",
|
|
140
|
+
"--no-zygote",
|
|
141
|
+
"--disable-gpu",
|
|
142
|
+
]
|
|
143
|
+
launch_args.extend(self.config.extra_args)
|
|
144
|
+
|
|
145
|
+
# Proxy configuration
|
|
146
|
+
proxy_settings = None
|
|
147
|
+
if self.config.proxy:
|
|
148
|
+
proxy_settings = {"server": self.config.proxy}
|
|
149
|
+
|
|
150
|
+
self._browser = self._playwright.chromium.launch(
|
|
151
|
+
headless=self.config.headless,
|
|
152
|
+
args=launch_args,
|
|
153
|
+
proxy=proxy_settings,
|
|
154
|
+
slow_mo=self.config.slow_mo,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Create context with stealth settings
|
|
158
|
+
self._context = self._create_stealth_context()
|
|
159
|
+
|
|
160
|
+
# Load saved cookies if they exist
|
|
161
|
+
self._load_cookies()
|
|
162
|
+
|
|
163
|
+
logger.info(
|
|
164
|
+
f"Browser launched (headless={self.config.headless}, "
|
|
165
|
+
f"proxy={'yes' if self.config.proxy else 'no'})"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def new_page(self) -> Page:
|
|
169
|
+
"""Create a new browser page.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A new Playwright Page object.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
RuntimeError: If browser is not launched.
|
|
176
|
+
"""
|
|
177
|
+
if not self._context:
|
|
178
|
+
raise RuntimeError("Browser not launched. Call launch() first.")
|
|
179
|
+
|
|
180
|
+
page = self._context.new_page()
|
|
181
|
+
self._pages.append(page)
|
|
182
|
+
|
|
183
|
+
# Inject stealth scripts
|
|
184
|
+
self._inject_stealth_scripts(page)
|
|
185
|
+
|
|
186
|
+
logger.debug("New page created")
|
|
187
|
+
return page
|
|
188
|
+
|
|
189
|
+
def close(self) -> None:
|
|
190
|
+
"""Close all pages, context, browser, and playwright.
|
|
191
|
+
|
|
192
|
+
Saves cookies before closing.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
self._save_cookies()
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning(f"Failed to save cookies on close: {e}")
|
|
198
|
+
|
|
199
|
+
for page in self._pages:
|
|
200
|
+
try:
|
|
201
|
+
page.close()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
self._pages.clear()
|
|
205
|
+
|
|
206
|
+
if self._context:
|
|
207
|
+
try:
|
|
208
|
+
self._context.close()
|
|
209
|
+
except Exception:
|
|
210
|
+
pass
|
|
211
|
+
self._context = None
|
|
212
|
+
|
|
213
|
+
if self._browser:
|
|
214
|
+
try:
|
|
215
|
+
self._browser.close()
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
self._browser = None
|
|
219
|
+
|
|
220
|
+
if self._playwright:
|
|
221
|
+
try:
|
|
222
|
+
self._playwright.stop()
|
|
223
|
+
except Exception:
|
|
224
|
+
pass
|
|
225
|
+
self._playwright = None
|
|
226
|
+
|
|
227
|
+
logger.info("Browser closed")
|
|
228
|
+
|
|
229
|
+
def screenshot(self, page: Page, name: str) -> Optional[str]:
|
|
230
|
+
"""Take a screenshot of the current page.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
page: The Playwright Page to screenshot.
|
|
234
|
+
name: Filename for the screenshot.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Path to saved screenshot, or None if failed.
|
|
238
|
+
"""
|
|
239
|
+
try:
|
|
240
|
+
if self.config.screenshot_dir:
|
|
241
|
+
ss_dir = self.config.screenshot_dir
|
|
242
|
+
else:
|
|
243
|
+
ss_dir = self._session_dir / "screenshots"
|
|
244
|
+
ss_dir.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
|
|
246
|
+
path = ss_dir / name
|
|
247
|
+
page.screenshot(path=str(path), full_page=True)
|
|
248
|
+
logger.debug(f"Screenshot saved: {path}")
|
|
249
|
+
return str(path)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning(f"Screenshot failed: {e}")
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
def save_cookies(self) -> None:
|
|
255
|
+
"""Explicitly save current cookies to disk."""
|
|
256
|
+
self._save_cookies()
|
|
257
|
+
|
|
258
|
+
def load_cookies(self) -> None:
|
|
259
|
+
"""Explicitly load cookies from disk."""
|
|
260
|
+
self._load_cookies()
|
|
261
|
+
|
|
262
|
+
def get_session_path(self, platform: str = "") -> Path:
|
|
263
|
+
"""Get the session directory path for a platform.
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
platform: Platform name (e.g., 'twitter', 'discord').
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Path to the session directory.
|
|
270
|
+
"""
|
|
271
|
+
if platform:
|
|
272
|
+
p = self._session_dir / platform
|
|
273
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
274
|
+
return p
|
|
275
|
+
return self._session_dir
|
|
276
|
+
|
|
277
|
+
def navigate_with_retry(self, page: Page, url: str, timeout: int = 30000) -> bool:
|
|
278
|
+
"""Navigate to a URL with retry on failure.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
page: The Playwright Page.
|
|
282
|
+
url: URL to navigate to.
|
|
283
|
+
timeout: Navigation timeout in milliseconds.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
True if navigation succeeded.
|
|
287
|
+
"""
|
|
288
|
+
for attempt in range(self.config.max_retries):
|
|
289
|
+
try:
|
|
290
|
+
page.goto(url, timeout=timeout, wait_until="domcontentloaded")
|
|
291
|
+
return True
|
|
292
|
+
except Exception as e:
|
|
293
|
+
logger.warning(f"Navigation failed (attempt {attempt + 1}): {e}")
|
|
294
|
+
if attempt < self.config.max_retries - 1:
|
|
295
|
+
time.sleep(self.config.retry_delay * (attempt + 1))
|
|
296
|
+
# Try to recover from crash
|
|
297
|
+
try:
|
|
298
|
+
if page.is_closed():
|
|
299
|
+
page = self.new_page()
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
def wait_for_element(
|
|
305
|
+
self,
|
|
306
|
+
page: Page,
|
|
307
|
+
selector: str,
|
|
308
|
+
timeout: int = 10000,
|
|
309
|
+
) -> bool:
|
|
310
|
+
"""Wait for an element to appear on the page.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
page: The Playwright Page.
|
|
314
|
+
selector: CSS selector to wait for.
|
|
315
|
+
timeout: Max wait time in milliseconds.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
True if element appeared.
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
page.wait_for_selector(selector, timeout=timeout)
|
|
322
|
+
return True
|
|
323
|
+
except Exception:
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
def click_with_retry(self, page: Page, selector: str, timeout: int = 10000) -> bool:
|
|
327
|
+
"""Click an element with retry.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
page: The Playwright Page.
|
|
331
|
+
selector: CSS selector to click.
|
|
332
|
+
timeout: Max wait time in milliseconds.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
True if click succeeded.
|
|
336
|
+
"""
|
|
337
|
+
for attempt in range(self.config.max_retries):
|
|
338
|
+
try:
|
|
339
|
+
page.click(selector, timeout=timeout)
|
|
340
|
+
return True
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.debug(f"Click failed (attempt {attempt + 1}): {e}")
|
|
343
|
+
if attempt < self.config.max_retries - 1:
|
|
344
|
+
time.sleep(1)
|
|
345
|
+
return False
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def is_launched(self) -> bool:
|
|
349
|
+
"""Check if the browser is currently launched."""
|
|
350
|
+
return self._browser is not None and self._context is not None
|
|
351
|
+
|
|
352
|
+
def __enter__(self):
|
|
353
|
+
self.launch()
|
|
354
|
+
return self
|
|
355
|
+
|
|
356
|
+
def __exit__(self, *args):
|
|
357
|
+
self.close()
|
|
358
|
+
|
|
359
|
+
# --- Private helpers ---
|
|
360
|
+
|
|
361
|
+
def _create_stealth_context(self) -> BrowserContext:
|
|
362
|
+
"""Create a browser context with stealth settings."""
|
|
363
|
+
ua = self.config.user_agent or random.choice(USER_AGENTS)
|
|
364
|
+
tz = self.config.timezone or random.choice(TIMEZONES)
|
|
365
|
+
vp = self.config.viewport or random.choice(VIEWPORTS)
|
|
366
|
+
|
|
367
|
+
context = self._browser.new_context( # type: ignore[union-attr]
|
|
368
|
+
user_agent=ua,
|
|
369
|
+
viewport=vp,
|
|
370
|
+
locale="en-US",
|
|
371
|
+
timezone_id=tz,
|
|
372
|
+
# Reduce detection signals
|
|
373
|
+
java_script_enabled=True,
|
|
374
|
+
has_touch=False,
|
|
375
|
+
is_mobile=False,
|
|
376
|
+
color_scheme="light",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Override navigator.webdriver
|
|
380
|
+
context.add_init_script("""
|
|
381
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
382
|
+
get: () => undefined
|
|
383
|
+
});
|
|
384
|
+
Object.defineProperty(navigator, 'languages', {
|
|
385
|
+
get: () => ['en-US', 'en']
|
|
386
|
+
});
|
|
387
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
388
|
+
get: () => [1, 2, 3, 4, 5]
|
|
389
|
+
});
|
|
390
|
+
window.chrome = { runtime: {} };
|
|
391
|
+
""")
|
|
392
|
+
|
|
393
|
+
return context
|
|
394
|
+
|
|
395
|
+
def _inject_stealth_scripts(self, page: Page) -> None:
|
|
396
|
+
"""Inject additional stealth scripts into a page."""
|
|
397
|
+
try:
|
|
398
|
+
page.add_init_script("""
|
|
399
|
+
// Fake notification permission
|
|
400
|
+
const originalQuery = window.navigator.permissions.query;
|
|
401
|
+
window.navigator.permissions.query = (parameters) =>
|
|
402
|
+
parameters.name === 'notifications'
|
|
403
|
+
? Promise.resolve({ state: Notification.permission })
|
|
404
|
+
: originalQuery(parameters);
|
|
405
|
+
""")
|
|
406
|
+
except Exception:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
def _save_cookies(self) -> None:
|
|
410
|
+
"""Save context cookies to disk."""
|
|
411
|
+
if not self._context:
|
|
412
|
+
return
|
|
413
|
+
try:
|
|
414
|
+
cookies = self._context.cookies()
|
|
415
|
+
cookie_file = self._session_dir / "cookies.json"
|
|
416
|
+
cookie_file.parent.mkdir(parents=True, exist_ok=True)
|
|
417
|
+
with open(cookie_file, "w") as f:
|
|
418
|
+
json.dump(cookies, f, indent=2)
|
|
419
|
+
logger.debug(f"Saved {len(cookies)} cookies to {cookie_file}")
|
|
420
|
+
except Exception as e:
|
|
421
|
+
logger.warning(f"Cookie save failed: {e}")
|
|
422
|
+
|
|
423
|
+
def _load_cookies(self) -> None:
|
|
424
|
+
"""Load cookies from disk into context."""
|
|
425
|
+
cookie_file = self._session_dir / "cookies.json"
|
|
426
|
+
if not cookie_file.exists():
|
|
427
|
+
return
|
|
428
|
+
try:
|
|
429
|
+
with open(cookie_file) as f:
|
|
430
|
+
cookies = json.load(f)
|
|
431
|
+
if cookies and self._context:
|
|
432
|
+
self._context.add_cookies(cookies)
|
|
433
|
+
logger.debug(f"Loaded {len(cookies)} cookies from {cookie_file}")
|
|
434
|
+
except Exception as e:
|
|
435
|
+
logger.warning(f"Cookie load failed: {e}")
|