Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 44 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@

**Python 3.9+ library to solve CAPTCHAs automatically using RuCaptcha, 2Captcha, DeathByCaptcha, or CaptchaAI services.**

> **Using CaptchaAI:** pass `service_type=ServiceEnm.CAPTCHAAI` to any captcha class. CaptchaAI is
> 2Captcha-API-compatible via the classic `in.php`/`res.php` endpoints; supported task types: Turnstile,
> reCAPTCHA v2 (incl. Enterprise), reCAPTCHA v3, and ImageToText.
> **Using CaptchaAI:** compatible high-level classes can pass `service_type=ServiceEnm.CAPTCHAAI`.
> CaptchaAI uses the classic multipart `in.php`/`res.php` endpoints.
> ```python
> from python_rucaptcha.turnstile import Turnstile
> from python_rucaptcha.core.enums import ServiceEnm
Expand All @@ -21,6 +20,48 @@
> userAgent="Mozilla/5.0 ...",
> ).captcha_handler()
> ```
>
> Proxy task types require `proxyType`, `proxyAddress`, and `proxyPort`; credentials are optional:
> ```python
> from python_rucaptcha.re_captcha import ReCaptcha
> from python_rucaptcha.core.enums import ReCaptchaEnm, ServiceEnm
>
> result = ReCaptcha(
> rucaptcha_key="CAPTCHAAI_KEY",
> service_type=ServiceEnm.CAPTCHAAI,
> websiteURL="https://example.com/login",
> websiteKey="SITE_KEY",
> method=ReCaptchaEnm.RecaptchaV2Task,
> proxyType="HTTPS",
> proxyAddress="203.0.113.7",
> proxyPort=3128,
> proxyLogin="user",
> proxyPassword="pass",
> ).captcha_handler()
> ```
>
> For every documented CaptchaAI method—or a provider method released after this package—use the native,
> type-free client. `profile` validates a packaged documented contract; omit it to pass provider-native fields
> through unchanged.
> ```python
> from python_rucaptcha.captchaai import CaptchaAI, CaptchaAIFile
>
> result = CaptchaAI(
> rucaptcha_key="CAPTCHAAI_KEY",
> profile="cloudflare-challenge",
> params={
> "pageurl": "https://example.com/protected",
> "proxy": "user:pass@203.0.113.7:3128",
> "proxytype": "HTTPS",
> },
> ).captcha_handler()
>
> image = CaptchaAI(
> rucaptcha_key="CAPTCHAAI_KEY",
> profile="normal-solve-file",
> files={"file": CaptchaAIFile(b"... PNG bytes ...", "captcha.png", "image/png")},
> ).captcha_handler()
> ```

## What is this?

Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Check our other projects here - `RedPandaDev group <https://red-panda-dev.xyz/bl
modules/captcha-fox/example.rst
modules/captcha-temu/example.rst
modules/captcha-vk/example.rst
modules/captchaai/example.rst
modules/yidun-necaptcha/example.rst
modules/yandex-smart-captcha/example.rst
modules/control/example.rst
Expand Down
66 changes: 66 additions & 0 deletions docs/modules/captchaai/example.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
CaptchaAI
=========

CaptchaAI uses a classic multipart ``in.php`` / ``res.php`` API. Existing
high-level solvers can use ``service_type=ServiceEnm.CAPTCHAAI`` where a
compatibility profile is available. For every documented CaptchaAI method, use
the native client with provider field names.

Documented profile
------------------

A profile validates the documented required fields and supplies documented
defaults. The profile list is available through ``CaptchaAI.profiles()``.

.. code-block:: python

from python_rucaptcha.captchaai import CaptchaAI

result = CaptchaAI(
rucaptcha_key="CAPTCHAAI_KEY",
profile="cloudflare-challenge",
params={
"pageurl": "https://example.com/protected",
"proxy": "user:pass@203.0.113.7:3128",
"proxytype": "HTTPS",
},
).captcha_handler()

Future provider methods
-----------------------

The client does not restrict the native ``method`` string. A newly released
CaptchaAI classic API method can be used before this package adds a profile.

.. code-block:: python

from python_rucaptcha.captchaai import CaptchaAI

result = CaptchaAI(
rucaptcha_key="CAPTCHAAI_KEY",
method="provider_future_method",
params={"provider_field": "value"},
).captcha_handler()

File uploads and controls
-------------------------

Use ``CaptchaAIFile`` for API methods that require a real multipart file part,
such as Normal or Grid image CAPTCHA requests.

.. code-block:: python

from python_rucaptcha.captchaai import CaptchaAI, CaptchaAIFile

result = CaptchaAI(
rucaptcha_key="CAPTCHAAI_KEY",
profile="normal-solve-file",
files={"file": CaptchaAIFile(b"... image bytes ...", "captcha.png", "image/png")},
).captcha_handler()

client = CaptchaAI(rucaptcha_key="CAPTCHAAI_KEY", profile="turnstile", params={
"sitekey": "SITE_KEY",
"pageurl": "https://example.com",
})
balance = client.get_balance()
threads = client.get_threads_info()
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ dependencies = [
where = ["src"]
include = ["python_rucaptcha*"]

[tool.setuptools.package-data]
python_rucaptcha = ["core/data/*.json"]

[tool.setuptools.dynamic]
version = {attr = "python_rucaptcha.__version__"}

Expand Down
2 changes: 1 addition & 1 deletion src/python_rucaptcha/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "6.6.0"
__version__ = "6.7.0"
160 changes: 160 additions & 0 deletions src/python_rucaptcha/captchaai.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""Native, future-compatible CaptchaAI classic API client."""

from __future__ import annotations

from typing import Any, Mapping

import requests
from requests.adapters import HTTPAdapter

from .core import captchaai as transport
from .core.enums import ServiceEnm
from .core.config import RETRIES
from .core.serializer import CaptchaOptionsSer

CaptchaAIFile = transport.CaptchaAIFile


class CaptchaAI:
"""Client for CaptchaAI's classic multipart API.

A packaged profile can validate a documented method contract. Omitting the
profile allows provider-native parameters for methods not yet represented
in the package metadata.

Attributes:
key: CaptchaAI API key used for requests.
method: Provider method submitted by the client.
params: Provider-native scalar parameters.
files: Multipart file parts keyed by provider field name.
profile: Optional packaged profile name.
poll: Whether to poll for a result after submission.
"""

def __init__(
self,
rucaptcha_key: str,
method: str | None = None,
params: Mapping[str, Any] | None = None,
*,
files: Mapping[str, CaptchaAIFile] | None = None,
profile: str | None = None,
poll: bool | None = None,
sleep_time: int = 10,
):
"""Create a client for one CaptchaAI method.

Args:
rucaptcha_key: API key used for CaptchaAI requests.
method: Provider-native method name. Required unless ``profile``
supplies the method.
params: Provider-native scalar parameters.
files: Multipart file parts keyed by provider field name.
profile: Optional packaged profile used for validation and defaults.
poll: Whether to poll after submission. If omitted, the profile
determines the default and native methods poll by default.
sleep_time: Seconds between result polls.

Raises:
ValueError: If neither ``method`` nor ``profile`` is supplied.
"""
if method is None:
if profile is None:
raise ValueError("CaptchaAI requires a method or a documented profile")
method = transport.profile_method(profile)

self.key = rucaptcha_key
self.method = method
self.params = dict(params or {})
self.files = dict(files or {})
self.profile = profile
self.poll = poll
self.options = CaptchaOptionsSer(sleep_time=sleep_time, service_type=ServiceEnm.CAPTCHAAI)
self.options.urls_set()
self.session = requests.Session()
self.session.mount("http://", HTTPAdapter(max_retries=RETRIES))
self.session.mount("https://", HTTPAdapter(max_retries=RETRIES))

@classmethod
def profiles(cls) -> tuple[str, ...]:
"""List the packaged CaptchaAI profile names.

Returns:
Profile names available for declarative validation.
"""
return transport.profiles()

def captcha_handler(self) -> dict[str, Any]:
"""Submit the configured method and optionally poll for its result.

Returns:
A normalized result mapping containing either the solution or an
error description.
"""
return transport.solve_native(
key=self.key,
method=self.method,
params=self.params,
files=self.files,
profile=self.profile,
poll=self.poll,
url_request=self.options.url_request,
url_response=self.options.url_response,
sleep_time=self.options.sleep_time,
session=self.session,
)

async def aio_captcha_handler(self) -> dict[str, Any]:
"""Asynchronously submit the method and optionally poll for its result.

Returns:
A normalized result mapping containing either the solution or an
error description.
"""
return await transport.aio_solve_native(
key=self.key,
method=self.method,
params=self.params,
files=self.files,
profile=self.profile,
poll=self.poll,
url_request=self.options.url_request,
url_response=self.options.url_response,
sleep_time=self.options.sleep_time,
)

def get_balance(self) -> dict[str, Any]:
"""Fetch the account balance through the CaptchaAI control API.

Returns:
The provider response with the balance value when successful, or
the normalized error mapping returned by the transport.
"""
return transport.control(self.key, "balance", self.options.url_response, session=self.session)

async def aio_get_balance(self) -> dict[str, Any]:
"""Asynchronously fetch the account balance.

Returns:
The provider response with the balance value when successful, or
the normalized error mapping returned by the transport.
"""
return await transport.aio_control(self.key, "balance", self.options.url_response)

def get_threads_info(self) -> dict[str, Any]:
"""Fetch total and active thread information.

Returns:
The provider response with thread information, or the normalized
error mapping returned by the transport.
"""
return transport.control(self.key, "threads_info", self.options.url_response, session=self.session)

async def aio_get_threads_info(self) -> dict[str, Any]:
"""Asynchronously fetch total and active thread information.

Returns:
The provider response with thread information, or the normalized
error mapping returned by the transport.
"""
return await transport.aio_control(self.key, "threads_info", self.options.url_response)
1 change: 1 addition & 0 deletions src/python_rucaptcha/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def _processing_response(self, **kwargs: dict[str, Any]) -> dict[str, Any]:
url_request=self.params.url_request,
url_response=self.params.url_response,
sleep_time=self.params.sleep_time,
session=self.session,
)
try:
response = GetTaskResultResponseSer(
Expand Down
Loading
Loading