capsolver-core 0.1.0__py3-none-any.whl
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.
- capsolver_core/__init__.py +44 -0
- capsolver_core/__main__.py +99 -0
- capsolver_core/browser/__init__.py +6 -0
- capsolver_core/browser/adapter.py +116 -0
- capsolver_core/browser/driver.py +26 -0
- capsolver_core/browser/inject/__init__.py +1 -0
- capsolver_core/browser/inject/cloudflare.py +67 -0
- capsolver_core/browser/inject/recaptcha.py +162 -0
- capsolver_core/capsolver.py +206 -0
- capsolver_core/captcha/__init__.py +20 -0
- capsolver_core/captcha/handler.py +55 -0
- capsolver_core/captcha/handlers/__init__.py +20 -0
- capsolver_core/captcha/handlers/cloudflare.py +83 -0
- capsolver_core/captcha/handlers/recaptcha.py +106 -0
- capsolver_core/captcha/handlers/support.py +30 -0
- capsolver_core/captcha/registry.py +56 -0
- capsolver_core/captcha/types.py +60 -0
- capsolver_core/core/__init__.py +7 -0
- capsolver_core/core/client.py +163 -0
- capsolver_core/core/errors.py +55 -0
- capsolver_core/core/http.py +137 -0
- capsolver_core/core/tasks.py +134 -0
- capsolver_core/core/types.py +157 -0
- capsolver_core/py.typed +0 -0
- capsolver_core-0.1.0.dist-info/METADATA +206 -0
- capsolver_core-0.1.0.dist-info/RECORD +29 -0
- capsolver_core-0.1.0.dist-info/WHEEL +4 -0
- capsolver_core-0.1.0.dist-info/entry_points.txt +2 -0
- capsolver_core-0.1.0.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Capsolver — the public, agent-facing entry point.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's capsolver.ts. Wires a ``CapsolverClient``
|
|
4
|
+
(token-solving core) to a ``HandlerRegistry`` of per-captcha handlers.
|
|
5
|
+
|
|
6
|
+
The API key is optional at construction so read-only calls like
|
|
7
|
+
``get_supported_captchas()`` work without one; it is required (and
|
|
8
|
+
validated lazily) the first time a solve is attempted.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from capsolver_core.core.client import CapsolverClient, CapsolverClientOptions, WaitOptions
|
|
17
|
+
from capsolver_core.core.errors import CapsolverError
|
|
18
|
+
from capsolver_core.core.types import CaptchaType, BalanceResp
|
|
19
|
+
from capsolver_core.captcha.handler import CaptchaHandler
|
|
20
|
+
from capsolver_core.captcha.handlers import default_handlers
|
|
21
|
+
from capsolver_core.captcha.registry import HandlerRegistry
|
|
22
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
23
|
+
from capsolver_core.browser.adapter import to_driver
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SolveOnPageOptions:
|
|
28
|
+
"""Options for ``Capsolver.solve_on_page``."""
|
|
29
|
+
|
|
30
|
+
autofill: bool = True
|
|
31
|
+
throw_on_error: bool = False
|
|
32
|
+
timeout: float | None = None
|
|
33
|
+
polling_interval: float | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SolveOnPageResult:
|
|
38
|
+
"""Per-captcha result from ``solve_on_page``."""
|
|
39
|
+
|
|
40
|
+
info: CaptchaInfo
|
|
41
|
+
solution: Solution | None = None
|
|
42
|
+
filled: bool | None = None
|
|
43
|
+
error: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Capsolver:
|
|
47
|
+
"""Main SDK entry point — detect, solve, and autofill captchas."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
api_key: str = "",
|
|
53
|
+
service: str = "https://api.capsolver.com",
|
|
54
|
+
default_timeout: float = 120.0,
|
|
55
|
+
polling_interval: float = 5.0,
|
|
56
|
+
request_timeout_ms: int = 30_000,
|
|
57
|
+
app_id: str | None = None,
|
|
58
|
+
source: str | None = None,
|
|
59
|
+
version: str | None = None,
|
|
60
|
+
on_error: Any = None,
|
|
61
|
+
handlers: list[CaptchaHandler] | None = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
self._client_options = CapsolverClientOptions(
|
|
64
|
+
api_key=api_key,
|
|
65
|
+
service=service,
|
|
66
|
+
default_timeout=default_timeout,
|
|
67
|
+
polling_interval=polling_interval,
|
|
68
|
+
request_timeout_ms=request_timeout_ms,
|
|
69
|
+
app_id=app_id,
|
|
70
|
+
source=source,
|
|
71
|
+
version=version,
|
|
72
|
+
on_error=on_error,
|
|
73
|
+
)
|
|
74
|
+
self._registry = HandlerRegistry()
|
|
75
|
+
self._client: CapsolverClient | None = None
|
|
76
|
+
|
|
77
|
+
for h in handlers if handlers is not None else default_handlers():
|
|
78
|
+
self._registry.register(h)
|
|
79
|
+
|
|
80
|
+
# ── resource management ───────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
async def aclose(self) -> None:
|
|
83
|
+
"""Close the underlying HTTP client and release connections."""
|
|
84
|
+
if self._client is not None:
|
|
85
|
+
await self._client.aclose()
|
|
86
|
+
|
|
87
|
+
async def __aenter__(self) -> Capsolver:
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
91
|
+
await self.aclose()
|
|
92
|
+
|
|
93
|
+
# ── registry access ───────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def register(self, handler: CaptchaHandler) -> Capsolver:
|
|
96
|
+
"""Add or replace a captcha handler."""
|
|
97
|
+
self._registry.register(handler)
|
|
98
|
+
return self
|
|
99
|
+
|
|
100
|
+
def get_supported_captchas(self) -> list[str]:
|
|
101
|
+
"""Names of every registered handler."""
|
|
102
|
+
return self._registry.list()
|
|
103
|
+
|
|
104
|
+
def get_handler(self, key: str | CaptchaType) -> CaptchaHandler | None:
|
|
105
|
+
"""Resolve a handler by name or captcha type."""
|
|
106
|
+
return self._registry.resolve(key)
|
|
107
|
+
|
|
108
|
+
# ── solving ───────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
async def solve(self, info: CaptchaInfo, wait_options: WaitOptions | None = None) -> Solution:
|
|
111
|
+
"""Solve a captcha from its normalized info (token mode)."""
|
|
112
|
+
if not info or not info.type:
|
|
113
|
+
raise CapsolverError("CaptchaInfo.type is required to pick a handler")
|
|
114
|
+
|
|
115
|
+
handler = self._registry.resolve(info.type)
|
|
116
|
+
if not handler:
|
|
117
|
+
raise CapsolverError(f"No handler registered for captcha type: {info.type}")
|
|
118
|
+
|
|
119
|
+
return await handler.solve(info, self._get_client(), wait_options)
|
|
120
|
+
|
|
121
|
+
# ── browser-aware methods ─────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
async def detect(self, page: Any) -> list[CaptchaType]:
|
|
124
|
+
"""Which captcha types are present on a page. Returns ``[]`` when none.
|
|
125
|
+
|
|
126
|
+
Derived from :meth:`get_captcha_info` so the reported type reflects the
|
|
127
|
+
actual widget — e.g. reCAPTCHA v3 is returned as ``RECAPTCHA_V3``, not
|
|
128
|
+
the handler's canonical ``RECAPTCHA_V2`` family. Order-preserving and
|
|
129
|
+
de-duplicated.
|
|
130
|
+
"""
|
|
131
|
+
driver = to_driver(page)
|
|
132
|
+
found: list[CaptchaType] = []
|
|
133
|
+
for info in await self.get_captcha_info(driver):
|
|
134
|
+
if info.type not in found:
|
|
135
|
+
found.append(info.type)
|
|
136
|
+
return found
|
|
137
|
+
|
|
138
|
+
async def get_captcha_info(self, page: Any) -> list[CaptchaInfo]:
|
|
139
|
+
"""Structured params for every captcha on the page. Returns ``[]`` when none."""
|
|
140
|
+
driver = to_driver(page)
|
|
141
|
+
infos: list[CaptchaInfo] = []
|
|
142
|
+
for handler in self._registry.handlers():
|
|
143
|
+
try:
|
|
144
|
+
result = await handler.get_captcha_info(driver)
|
|
145
|
+
infos.extend(result)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
return infos
|
|
149
|
+
|
|
150
|
+
async def solve_on_page(
|
|
151
|
+
self,
|
|
152
|
+
page: Any,
|
|
153
|
+
options: SolveOnPageOptions | None = None,
|
|
154
|
+
) -> list[SolveOnPageResult]:
|
|
155
|
+
"""One-shot: detect → solve → (optionally) autofill.
|
|
156
|
+
|
|
157
|
+
Per-captcha errors are collected unless ``throw_on_error`` is set.
|
|
158
|
+
"""
|
|
159
|
+
opts = options or SolveOnPageOptions()
|
|
160
|
+
driver = to_driver(page)
|
|
161
|
+
infos = await self.get_captcha_info(driver)
|
|
162
|
+
|
|
163
|
+
wait_opts = WaitOptions(timeout=opts.timeout, polling_interval=opts.polling_interval)
|
|
164
|
+
|
|
165
|
+
results: list[SolveOnPageResult] = []
|
|
166
|
+
for info in infos:
|
|
167
|
+
result = SolveOnPageResult(info=info)
|
|
168
|
+
try:
|
|
169
|
+
handler = self._registry.resolve(info.type)
|
|
170
|
+
if not handler:
|
|
171
|
+
raise CapsolverError(f"No handler registered for captcha type: {info.type}")
|
|
172
|
+
|
|
173
|
+
result.solution = await handler.solve(info, self._get_client(), wait_opts)
|
|
174
|
+
|
|
175
|
+
if opts.autofill and hasattr(handler, "fill") and handler.fill is not None:
|
|
176
|
+
try:
|
|
177
|
+
result.filled = await handler.fill(driver, result.solution, info)
|
|
178
|
+
except Exception:
|
|
179
|
+
result.filled = False
|
|
180
|
+
except Exception as e:
|
|
181
|
+
if opts.throw_on_error:
|
|
182
|
+
raise
|
|
183
|
+
result.error = str(e)
|
|
184
|
+
results.append(result)
|
|
185
|
+
return results
|
|
186
|
+
|
|
187
|
+
# ── account ───────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
async def get_balance(self) -> BalanceResp:
|
|
190
|
+
"""Account balance (requires an API key)."""
|
|
191
|
+
return await self._get_client().get_balance()
|
|
192
|
+
|
|
193
|
+
# ── internals ─────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def _get_client(self) -> CapsolverClient:
|
|
196
|
+
"""Lazily construct the client, validating the API key on first use."""
|
|
197
|
+
if self._client is None:
|
|
198
|
+
if not self._client_options.api_key:
|
|
199
|
+
raise CapsolverError("Capsolver: apiKey is required to solve. Pass it to the constructor.")
|
|
200
|
+
self._client = CapsolverClient(self._client_options)
|
|
201
|
+
return self._client
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def create_capsolver(**kwargs: Any) -> Capsolver:
|
|
205
|
+
"""Convenience factory mirroring ``Capsolver(...)``."""
|
|
206
|
+
return Capsolver(**kwargs)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Captcha layer — handler protocol, registry, built-in handlers."""
|
|
2
|
+
|
|
3
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
4
|
+
from capsolver_core.captcha.handler import CaptchaHandler
|
|
5
|
+
from capsolver_core.captcha.registry import HandlerRegistry
|
|
6
|
+
from capsolver_core.captcha.handlers import (
|
|
7
|
+
RecaptchaHandler,
|
|
8
|
+
CloudflareHandler,
|
|
9
|
+
default_handlers,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CaptchaInfo",
|
|
14
|
+
"Solution",
|
|
15
|
+
"CaptchaHandler",
|
|
16
|
+
"HandlerRegistry",
|
|
17
|
+
"RecaptchaHandler",
|
|
18
|
+
"CloudflareHandler",
|
|
19
|
+
"default_handlers",
|
|
20
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""CaptchaHandler — the per-captcha plugin contract.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's captcha/handler.ts. Uses ``Protocol`` for
|
|
4
|
+
structural subtyping so third-party handlers don't need to inherit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Protocol, runtime_checkable
|
|
10
|
+
|
|
11
|
+
from capsolver_core.core.types import CaptchaType
|
|
12
|
+
from capsolver_core.core.client import CapsolverClient, WaitOptions
|
|
13
|
+
from capsolver_core.browser.driver import PageDriver
|
|
14
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@runtime_checkable
|
|
18
|
+
class CaptchaHandler(Protocol):
|
|
19
|
+
"""Per-captcha-family plugin interface."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def type(self) -> CaptchaType:
|
|
23
|
+
"""Canonical captcha family this handler serves."""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
"""Short, stable name used for registration."""
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def aliases(self) -> tuple[CaptchaType, ...]:
|
|
33
|
+
"""Extra captcha types this handler also serves."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
async def solve(
|
|
37
|
+
self,
|
|
38
|
+
info: CaptchaInfo,
|
|
39
|
+
client: CapsolverClient,
|
|
40
|
+
wait_options: WaitOptions | None = None,
|
|
41
|
+
) -> Solution:
|
|
42
|
+
"""Token-mode solve: build the task, poll, normalize the result."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def detect(self, page: PageDriver) -> bool:
|
|
46
|
+
"""Is this captcha present on the page?"""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def get_captcha_info(self, page: PageDriver) -> list[CaptchaInfo]:
|
|
50
|
+
"""Extract structured info for every instance on the page."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def fill(self, page: PageDriver, solution: Solution, info: CaptchaInfo) -> bool:
|
|
54
|
+
"""Write a solved token back into the page (autofill)."""
|
|
55
|
+
...
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Built-in captcha handlers."""
|
|
2
|
+
|
|
3
|
+
from capsolver_core.captcha.handlers.recaptcha import RecaptchaHandler
|
|
4
|
+
from capsolver_core.captcha.handlers.cloudflare import CloudflareHandler
|
|
5
|
+
from capsolver_core.captcha.handler import CaptchaHandler
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def default_handlers() -> list[CaptchaHandler]:
|
|
9
|
+
"""Fresh instances of every built-in token-mode handler."""
|
|
10
|
+
return [
|
|
11
|
+
RecaptchaHandler(),
|
|
12
|
+
CloudflareHandler(),
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"RecaptchaHandler",
|
|
18
|
+
"CloudflareHandler",
|
|
19
|
+
"default_handlers",
|
|
20
|
+
]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Cloudflare Turnstile handler (token mode).
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's captcha/handlers/cloudflare.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from capsolver_core.core.types import CaptchaType, TokenSolution
|
|
11
|
+
from capsolver_core.core.client import CapsolverClient, WaitOptions
|
|
12
|
+
from capsolver_core.core.tasks import build_cloudflare_task
|
|
13
|
+
from capsolver_core.browser.driver import PageDriver
|
|
14
|
+
from capsolver_core.browser.inject.cloudflare import (
|
|
15
|
+
DETECT_CLOUDFLARE_JS,
|
|
16
|
+
GET_CLOUDFLARE_INFOS_JS,
|
|
17
|
+
FILL_CLOUDFLARE_JS,
|
|
18
|
+
)
|
|
19
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
20
|
+
from capsolver_core.captcha.handlers.support import require_url_and_key, to_solution
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CloudflareHandler:
|
|
24
|
+
"""Cloudflare Turnstile token-mode handler."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def type(self) -> CaptchaType:
|
|
28
|
+
return CaptchaType.CLOUDFLARE
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return "cloudflare"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def aliases(self) -> tuple[CaptchaType, ...]:
|
|
36
|
+
return ()
|
|
37
|
+
|
|
38
|
+
async def solve(
|
|
39
|
+
self,
|
|
40
|
+
info: CaptchaInfo,
|
|
41
|
+
client: CapsolverClient,
|
|
42
|
+
wait_options: WaitOptions | None = None,
|
|
43
|
+
) -> Solution:
|
|
44
|
+
website_url, website_key = require_url_and_key(info)
|
|
45
|
+
task = build_cloudflare_task(
|
|
46
|
+
website_url=website_url,
|
|
47
|
+
website_key=website_key,
|
|
48
|
+
action=info.page_action,
|
|
49
|
+
cdata=info.cdata,
|
|
50
|
+
proxy=info.proxy,
|
|
51
|
+
)
|
|
52
|
+
res = await client.create_task_result(task, wait_options)
|
|
53
|
+
return to_solution(CaptchaType.CLOUDFLARE, TokenSolution.from_dict(res.get("solution")))
|
|
54
|
+
|
|
55
|
+
async def detect(self, page: PageDriver) -> bool:
|
|
56
|
+
return bool(await page.evaluate(DETECT_CLOUDFLARE_JS))
|
|
57
|
+
|
|
58
|
+
async def get_captcha_info(self, page: PageDriver) -> list[CaptchaInfo]:
|
|
59
|
+
url = await page.url()
|
|
60
|
+
raws: list[dict[str, Any]] = await page.evaluate(GET_CLOUDFLARE_INFOS_JS)
|
|
61
|
+
infos: list[CaptchaInfo] = []
|
|
62
|
+
for r in raws:
|
|
63
|
+
if not r.get("websiteKey"):
|
|
64
|
+
continue
|
|
65
|
+
infos.append(
|
|
66
|
+
CaptchaInfo(
|
|
67
|
+
type=CaptchaType.CLOUDFLARE,
|
|
68
|
+
website_url=url,
|
|
69
|
+
website_key=r["websiteKey"],
|
|
70
|
+
page_action=r.get("action") or None,
|
|
71
|
+
cdata=r.get("cdata") or None,
|
|
72
|
+
container_id=r.get("containerId"),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return infos
|
|
76
|
+
|
|
77
|
+
async def fill(self, page: PageDriver, solution: Solution, info: CaptchaInfo) -> bool:
|
|
78
|
+
return bool(
|
|
79
|
+
await page.evaluate(
|
|
80
|
+
FILL_CLOUDFLARE_JS,
|
|
81
|
+
{"token": solution.token, "containerId": info.container_id},
|
|
82
|
+
)
|
|
83
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""reCAPTCHA handler — covers v2 and v3 (incl. enterprise) in token mode.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's captcha/handlers/recaptcha.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from capsolver_core.core.types import CaptchaType, TokenSolution
|
|
11
|
+
from capsolver_core.core.client import CapsolverClient, WaitOptions
|
|
12
|
+
from capsolver_core.core.tasks import build_recaptcha_v2_task, build_recaptcha_v3_task
|
|
13
|
+
from capsolver_core.browser.driver import PageDriver
|
|
14
|
+
from capsolver_core.browser.inject.recaptcha import (
|
|
15
|
+
DETECT_RECAPTCHA_JS,
|
|
16
|
+
GET_RECAPTCHA_INFOS_JS,
|
|
17
|
+
FILL_RECAPTCHA_JS,
|
|
18
|
+
)
|
|
19
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
20
|
+
from capsolver_core.captcha.handlers.support import require_url_and_key, to_solution
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RecaptchaHandler:
|
|
24
|
+
"""reCAPTCHA v2 + v3 (incl. enterprise) token-mode handler."""
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def type(self) -> CaptchaType:
|
|
28
|
+
return CaptchaType.RECAPTCHA_V2
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return "recaptcha"
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def aliases(self) -> tuple[CaptchaType, ...]:
|
|
36
|
+
return (CaptchaType.RECAPTCHA_V3,)
|
|
37
|
+
|
|
38
|
+
async def solve(
|
|
39
|
+
self,
|
|
40
|
+
info: CaptchaInfo,
|
|
41
|
+
client: CapsolverClient,
|
|
42
|
+
wait_options: WaitOptions | None = None,
|
|
43
|
+
) -> Solution:
|
|
44
|
+
website_url, website_key = require_url_and_key(info)
|
|
45
|
+
enterprise_payload = {"s": info.s} if info.s else None
|
|
46
|
+
is_v3 = info.version == "v3" or info.type == CaptchaType.RECAPTCHA_V3
|
|
47
|
+
|
|
48
|
+
if is_v3:
|
|
49
|
+
task = build_recaptcha_v3_task(
|
|
50
|
+
website_url=website_url,
|
|
51
|
+
website_key=website_key,
|
|
52
|
+
page_action=info.page_action,
|
|
53
|
+
min_score=info.min_score,
|
|
54
|
+
enterprise=info.enterprise,
|
|
55
|
+
enterprise_payload=enterprise_payload,
|
|
56
|
+
proxy=info.proxy,
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
task = build_recaptcha_v2_task(
|
|
60
|
+
website_url=website_url,
|
|
61
|
+
website_key=website_key,
|
|
62
|
+
invisible=info.invisible,
|
|
63
|
+
page_action=info.page_action,
|
|
64
|
+
enterprise=info.enterprise,
|
|
65
|
+
enterprise_payload=enterprise_payload,
|
|
66
|
+
proxy=info.proxy,
|
|
67
|
+
user_agent=info.user_agent,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
res = await client.create_task_result(task, wait_options)
|
|
71
|
+
return to_solution(info.type, TokenSolution.from_dict(res.get("solution")))
|
|
72
|
+
|
|
73
|
+
async def detect(self, page: PageDriver) -> bool:
|
|
74
|
+
return bool(await page.evaluate(DETECT_RECAPTCHA_JS))
|
|
75
|
+
|
|
76
|
+
async def get_captcha_info(self, page: PageDriver) -> list[CaptchaInfo]:
|
|
77
|
+
url = await page.url()
|
|
78
|
+
raws: list[dict[str, Any]] = await page.evaluate(GET_RECAPTCHA_INFOS_JS)
|
|
79
|
+
infos: list[CaptchaInfo] = []
|
|
80
|
+
for r in raws:
|
|
81
|
+
if not r.get("sitekey"):
|
|
82
|
+
continue
|
|
83
|
+
infos.append(
|
|
84
|
+
CaptchaInfo(
|
|
85
|
+
type=CaptchaType.RECAPTCHA_V3 if r.get("version") == "v3" else CaptchaType.RECAPTCHA_V2,
|
|
86
|
+
version=r.get("version"),
|
|
87
|
+
website_url=url,
|
|
88
|
+
website_key=r["sitekey"],
|
|
89
|
+
page_action=r.get("action") or None,
|
|
90
|
+
invisible=r.get("invisible"),
|
|
91
|
+
enterprise=r.get("enterprise"),
|
|
92
|
+
s=r.get("s") or None,
|
|
93
|
+
container_id=r.get("containerId"),
|
|
94
|
+
callback=r.get("callback"),
|
|
95
|
+
binded_button_id=r.get("bindedButtonId"),
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return infos
|
|
99
|
+
|
|
100
|
+
async def fill(self, page: PageDriver, solution: Solution, info: CaptchaInfo) -> bool:
|
|
101
|
+
return bool(
|
|
102
|
+
await page.evaluate(
|
|
103
|
+
FILL_RECAPTCHA_JS,
|
|
104
|
+
{"token": solution.token, "containerId": info.container_id, "callback": info.callback},
|
|
105
|
+
)
|
|
106
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Shared helpers for token-mode handlers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from capsolver_core.core.errors import CapsolverError
|
|
6
|
+
from capsolver_core.core.types import CaptchaType, TokenSolution
|
|
7
|
+
from capsolver_core.captcha.types import CaptchaInfo, Solution
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def require_url_and_key(info: CaptchaInfo) -> tuple[str, str]:
|
|
11
|
+
"""Ensure the fields every token task needs are present."""
|
|
12
|
+
if not info.website_url:
|
|
13
|
+
raise CapsolverError("CaptchaInfo.website_url is required to solve")
|
|
14
|
+
if not info.website_key:
|
|
15
|
+
raise CapsolverError("CaptchaInfo.website_key is required to solve")
|
|
16
|
+
return info.website_url, info.website_key
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def to_solution(captcha_type: CaptchaType, raw: TokenSolution | None) -> Solution:
|
|
20
|
+
"""Normalize an API solution payload into the SDK ``Solution`` shape."""
|
|
21
|
+
token = (raw.g_recaptcha_response if raw else None) or (raw.token if raw else None)
|
|
22
|
+
if not token:
|
|
23
|
+
raise CapsolverError("Solver returned an empty token", error_code="EMPTY_TOKEN")
|
|
24
|
+
return Solution(
|
|
25
|
+
captcha_type=captcha_type,
|
|
26
|
+
token=token,
|
|
27
|
+
raw=raw,
|
|
28
|
+
expire_time=raw.expire_time if raw else None,
|
|
29
|
+
user_agent=raw.user_agent if raw else None,
|
|
30
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""HandlerRegistry — register/lookup captcha handlers.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's captcha/registry.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import builtins
|
|
9
|
+
|
|
10
|
+
from capsolver_core.core.types import CaptchaType
|
|
11
|
+
from capsolver_core.captcha.handler import CaptchaHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HandlerRegistry:
|
|
15
|
+
"""Index handlers by short name and by every ``CaptchaType`` they serve."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._by_name: dict[str, CaptchaHandler] = {}
|
|
19
|
+
self._by_type: dict[CaptchaType, CaptchaHandler] = {}
|
|
20
|
+
|
|
21
|
+
def register(self, handler: CaptchaHandler) -> HandlerRegistry:
|
|
22
|
+
self._by_name[handler.name] = handler
|
|
23
|
+
self._by_type[handler.type] = handler
|
|
24
|
+
for alias in handler.aliases:
|
|
25
|
+
self._by_type[alias] = handler
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def get(self, name: str) -> CaptchaHandler | None:
|
|
29
|
+
return self._by_name.get(name)
|
|
30
|
+
|
|
31
|
+
def get_by_type(self, captcha_type: CaptchaType) -> CaptchaHandler | None:
|
|
32
|
+
return self._by_type.get(captcha_type)
|
|
33
|
+
|
|
34
|
+
def resolve(self, key: str | CaptchaType) -> CaptchaHandler | None:
|
|
35
|
+
"""Look up by name string or ``CaptchaType``."""
|
|
36
|
+
if isinstance(key, CaptchaType):
|
|
37
|
+
return self._by_type.get(key)
|
|
38
|
+
# Try by name first
|
|
39
|
+
handler = self._by_name.get(key)
|
|
40
|
+
if handler:
|
|
41
|
+
return handler
|
|
42
|
+
# Try converting string to CaptchaType (e.g. "reCaptchaV2")
|
|
43
|
+
try:
|
|
44
|
+
return self._by_type.get(CaptchaType(key))
|
|
45
|
+
except (ValueError, KeyError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def list(self) -> builtins.list[str]:
|
|
49
|
+
"""Registered handler names, in insertion order."""
|
|
50
|
+
return builtins.list(self._by_name.keys())
|
|
51
|
+
|
|
52
|
+
def handlers(self) -> builtins.list[CaptchaHandler]:
|
|
53
|
+
return builtins.list(self._by_name.values())
|
|
54
|
+
|
|
55
|
+
def has(self, name: str) -> bool:
|
|
56
|
+
return name in self._by_name
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Contract types shared between detection and solving.
|
|
2
|
+
|
|
3
|
+
Mirrors the Node SDK's captcha/types.ts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from capsolver_core.core.types import CaptchaType, TokenSolution
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CaptchaInfo:
|
|
16
|
+
"""Normalized, browser-agnostic description of a captcha on a page."""
|
|
17
|
+
|
|
18
|
+
type: CaptchaType
|
|
19
|
+
website_url: str = ""
|
|
20
|
+
website_key: str = ""
|
|
21
|
+
|
|
22
|
+
# reCAPTCHA-specific
|
|
23
|
+
version: str | None = None # "v2" | "v3"
|
|
24
|
+
page_action: str | None = None
|
|
25
|
+
invisible: bool | None = None
|
|
26
|
+
enterprise: bool | None = None
|
|
27
|
+
s: str | None = None # Enterprise ``s`` token
|
|
28
|
+
min_score: float | None = None # reCAPTCHA v3
|
|
29
|
+
|
|
30
|
+
# Cloudflare Turnstile
|
|
31
|
+
cdata: str | None = None
|
|
32
|
+
|
|
33
|
+
# Generic
|
|
34
|
+
proxy: str | None = None
|
|
35
|
+
user_agent: str | None = None
|
|
36
|
+
|
|
37
|
+
# DOM hooks (Phase 3 autofill)
|
|
38
|
+
container_id: str | None = None
|
|
39
|
+
callback: str | None = None
|
|
40
|
+
binded_button_id: str | None = None
|
|
41
|
+
|
|
42
|
+
# Carry-through for fields not yet modelled
|
|
43
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
if not self.website_url:
|
|
47
|
+
raise ValueError("CaptchaInfo.website_url is required")
|
|
48
|
+
if not self.website_key:
|
|
49
|
+
raise ValueError("CaptchaInfo.website_key is required")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Solution:
|
|
54
|
+
"""Normalized solve result."""
|
|
55
|
+
|
|
56
|
+
captcha_type: CaptchaType
|
|
57
|
+
token: str
|
|
58
|
+
raw: TokenSolution | None = None
|
|
59
|
+
expire_time: int | None = None
|
|
60
|
+
user_agent: str | None = None
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Core layer — HTTP, errors, task builders, client, types."""
|
|
2
|
+
|
|
3
|
+
from capsolver_core.core.types import * # noqa: F401,F403
|
|
4
|
+
from capsolver_core.core.errors import * # noqa: F401,F403
|
|
5
|
+
from capsolver_core.core.http import * # noqa: F401,F403
|
|
6
|
+
from capsolver_core.core.tasks import * # noqa: F401,F403
|
|
7
|
+
from capsolver_core.core.client import * # noqa: F401,F403
|