diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 16e814f..46b1740 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,10 @@ jobs: # to catch packaging errors early. Skipped if lint fails to save CI minutes. test: needs: lint - runs-on: ubuntu-latest + # Ubuntu 26.04 (the current ubuntu-latest) does not provide Python 3.9 + # through actions/setup-python. Pin the matrix runner while Python 3.9 is + # part of the SDK's supported version range. + runs-on: ubuntu-24.04 strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] diff --git a/README.md b/README.md index 8695653..e23adae 100644 --- a/README.md +++ b/README.md @@ -499,11 +499,9 @@ With proxy, use `GeeTestTask` (same proxy fields as reCAPTCHA v2). [API method description.](https://captcha-solver.com/en/docs/captcha-types#yandex-smartcaptcha) Use this method to solve the token-based Yandex SmartCaptcha and obtain a token -for the widget on the target page. Use the coordinates method for image challenges. +for the widget on the target page. Use the dedicated image method for image challenges. -`YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based -challenge. For the image challenge instead, use `CoordinatesTask` with -[Coordinates](#coordinates-click-captcha) is available for generic click captchas. +`YandexSmartCaptchaTaskProxyless` / `YandexSmartCaptchaTask` -- token-based challenge. | Parameter | Required | Description | |---|---|---| @@ -532,15 +530,45 @@ print(result["token"]) With proxy, use `YandexSmartCaptchaTask` (same proxy fields as reCAPTCHA v2, plus the `https` option above). +#### Image challenge + +Use `YandexSmartCaptchaImageTask` when SmartCaptcha shows a main image and a +separate strip indicating the symbols to click. The SDK sends API v2 type +`SmartCaptchaTask`; both images must be base64-encoded. + +| Parameter | Required | Description | +|---|---|---| +| `image` | yes | Main captcha image, base64-encoded. | +| `imgInstructions` | yes | Click-order instruction image, base64-encoded. | +| `comment` | no | Optional text hint for the worker. | + +**Response:** `coordinates` -- ordered `{"x": int, "y": int}` click positions. + +```python +import base64 +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import YandexSmartCaptchaImageTask + +with open("captcha.png", "rb") as f: + image = base64.b64encode(f.read()).decode("utf-8") +with open("instructions.png", "rb") as f: + instructions = base64.b64encode(f.read()).decode("utf-8") + +task = YandexSmartCaptchaImageTask( + image=image, + imgInstructions=instructions, +) +result = CaptchaClient("your_api_key").solve(task) +print(result["coordinates"]) +``` + ### Coordinates (click captcha) [API method description.](https://captcha-solver.com/en/docs/captcha-types#coordinates) -Use this method to identify points that a worker should click in an image. It -supports generic click captchas and the image version of Yandex SmartCaptcha. +Use this method to identify points that a worker should click in a generic image. -`CoordinatesTask`. Used both for generic "click on X" captchas and for Yandex -SmartCaptcha's image challenge. No proxy variant -- the image is submitted directly. +`CoordinatesTask`. No proxy variant -- the image is submitted directly. | Parameter | Required | Description | |---|---|---| @@ -718,7 +746,8 @@ Avoid importing it by name: it shadows Python's built-in `TimeoutError`. See the dedicated [examples documentation](https://github.com/captcha-solver-api/python-sdk/blob/main/examples/README.md) for the full sync/async example list, setup steps, expected results, and placeholder guidance. -- **Image/click captchas** (`image_to_text.py`, `coordinates.py`) run after installing the dependencies and setting a valid +- **Image/click captchas** (`image_to_text.py`, `coordinates.py`, + `yandex_smartcaptcha_image.py`) run after installing the dependencies and setting a valid `CAPTCHA_API_KEY` -- they read sample images bundled in [examples/assets](https://github.com/captcha-solver-api/python-sdk/tree/main/examples/assets), no target page needed. `python-dotenv` is optional: install it only if you want the scripts to load a `.env` file. @@ -743,6 +772,7 @@ export CAPTCHA_API_KEY=your_api_key python examples/sync/balance.py python examples/sync/image_to_text.py python examples/sync/coordinates.py +python examples/sync/yandex_smartcaptcha_image.py ``` Live checks with real parameters have exercised all supported task types. diff --git a/captcha_solver_api/tasks.py b/captcha_solver_api/tasks.py index 5df677e..0f7a500 100644 --- a/captcha_solver_api/tasks.py +++ b/captcha_solver_api/tasks.py @@ -787,7 +787,7 @@ def __init__( class CoordinatesTask(BaseTask): """Coordinate-based (click) image captcha. No proxy variant -- the image - is submitted directly. + is submitted directly. Use this task for generic click captchas. Args: body: The captcha image, base64-encoded (no `data:image/...;base64,` prefix). @@ -832,6 +832,31 @@ def __init__( self.maxClicks = maxClicks +class YandexSmartCaptchaImageTask(BaseTask): + """Image-based Yandex SmartCaptcha using API v2 ``SmartCaptchaTask``. + + Args: + image: Main SmartCaptcha challenge image, base64-encoded. + imgInstructions: Click-order instruction image, base64-encoded. + comment: Optional text hint for the worker. + + Returns (``solution`` from ``solve()``): + ``coordinates`` -- an ordered list of ``{"x": int, "y": int}`` points. + """ + + type = "SmartCaptchaTask" + + def __init__( + self, + image: str, + imgInstructions: str, + comment: Optional[str] = None, + ) -> None: + self.image = image + self.imgInstructions = imgInstructions + self.comment = comment + + class TencentTaskProxyless(BaseTask): """Tencent captcha without proxy. diff --git a/examples/README.md b/examples/README.md index 00057ac..43772f8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -61,6 +61,7 @@ Each supported CAPTCHA type has a matching sync and async script: | GeeTest v3 | [geetest_v3.py](sync/geetest_v3.py) | [geetest_v3.py](async/geetest_v3.py) | `challenge`, `validate`, `seccode` | | GeeTest v4 | [geetest_v4.py](sync/geetest_v4.py) | [geetest_v4.py](async/geetest_v4.py) | `captcha_output` and related fields | | Yandex SmartCaptcha token | [yandex_smartcaptcha.py](sync/yandex_smartcaptcha.py) | [yandex_smartcaptcha.py](async/yandex_smartcaptcha.py) | `token` | +| Yandex SmartCaptcha image | [yandex_smartcaptcha_image.py](sync/yandex_smartcaptcha_image.py) | [yandex_smartcaptcha_image.py](async/yandex_smartcaptcha_image.py) | `coordinates` | | Coordinates / click captcha | [coordinates.py](sync/coordinates.py) | [coordinates.py](async/coordinates.py) | `coordinates` | | Tencent | [tencent.py](sync/tencent.py) | [tencent.py](async/tencent.py) | `appid`, `ret`, `ticket`, `randstr` | diff --git a/examples/assets/yandex-smartcaptcha-image.png b/examples/assets/yandex-smartcaptcha-image.png new file mode 100644 index 0000000..92addc3 Binary files /dev/null and b/examples/assets/yandex-smartcaptcha-image.png differ diff --git a/examples/assets/yandex-smartcaptcha-instructions.png b/examples/assets/yandex-smartcaptcha-instructions.png new file mode 100644 index 0000000..80cf090 Binary files /dev/null and b/examples/assets/yandex-smartcaptcha-instructions.png differ diff --git a/examples/async/yandex_smartcaptcha_image.py b/examples/async/yandex_smartcaptcha_image.py new file mode 100644 index 0000000..a89202c --- /dev/null +++ b/examples/async/yandex_smartcaptcha_image.py @@ -0,0 +1,54 @@ +"""Example: Solve an image-based Yandex SmartCaptcha asynchronously. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + The sample captcha and instruction images are bundled in examples/assets/. +""" + +import asyncio +import os +import sys +from base64 import b64encode + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import AsyncCaptchaClient +from captcha_solver_api.tasks import YandexSmartCaptchaImageTask + + +def encode_image(filename: str) -> str: + """Return an image from examples/assets as a Base64 string.""" + path = os.path.join(repo_root, "examples", "assets", filename) + with open(path, "rb") as image_file: + return b64encode(image_file.read()).decode("utf-8") + + +async def main() -> None: + api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") + async with AsyncCaptchaClient(api_key) as solver: + result = await solver.solve( + YandexSmartCaptchaImageTask( + image=encode_image("yandex-smartcaptcha-image.png"), + imgInstructions=encode_image("yandex-smartcaptcha-instructions.png"), + ) + ) + # Click the returned points on the captcha image in the listed order. + # Example: {"coordinates": [{"x": 73, "y": 92}, ...]} + print("result: " + str(result)) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except Exception as exc: + sys.exit(exc) diff --git a/examples/sync/yandex_smartcaptcha_image.py b/examples/sync/yandex_smartcaptcha_image.py new file mode 100644 index 0000000..9973cdc --- /dev/null +++ b/examples/sync/yandex_smartcaptcha_image.py @@ -0,0 +1,49 @@ +"""Example: Solve an image-based Yandex SmartCaptcha. + +Prerequisites: + Set the CAPTCHA_API_KEY environment variable. + The sample captcha and instruction images are bundled in examples/assets/. +""" + +import os +import sys +from base64 import b64encode + +try: + from dotenv import load_dotenv +except ModuleNotFoundError as exc: + if exc.name != "dotenv": + raise + load_dotenv = None + +repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) +sys.path.append(repo_root) +if load_dotenv is not None: + load_dotenv(os.path.join(repo_root, ".env")) + +from captcha_solver_api import CaptchaClient +from captcha_solver_api.tasks import YandexSmartCaptchaImageTask + + +def encode_image(filename: str) -> str: + """Return an image from examples/assets as a Base64 string.""" + path = os.path.join(repo_root, "examples", "assets", filename) + with open(path, "rb") as image_file: + return b64encode(image_file.read()).decode("utf-8") + + +api_key = os.getenv("CAPTCHA_API_KEY", "YOUR_API_KEY") +solver = CaptchaClient(api_key) + +try: + result = solver.solve( + YandexSmartCaptchaImageTask( + image=encode_image("yandex-smartcaptcha-image.png"), + imgInstructions=encode_image("yandex-smartcaptcha-instructions.png"), + ) + ) + # Click the returned points on the captcha image in the listed order. + # Example: {"coordinates": [{"x": 73, "y": 92}, ...]} + print("result: " + str(result)) +except Exception as exc: + sys.exit(exc) diff --git a/tests/sync/test_coordinates.py b/tests/sync/test_coordinates.py index d9509b2..ab26984 100644 --- a/tests/sync/test_coordinates.py +++ b/tests/sync/test_coordinates.py @@ -23,11 +23,13 @@ def test_to_dict(self): def test_with_click_limits(self): task = CoordinatesTask( body="base64string", + imgInstructions="base64instructions", minClicks=1, maxClicks=5, ) result = task.to_dict() + assert result["imgInstructions"] == "base64instructions" assert result["minClicks"] == 1 assert result["maxClicks"] == 5 diff --git a/tests/sync/test_yandex_smartcaptcha_image.py b/tests/sync/test_yandex_smartcaptcha_image.py new file mode 100644 index 0000000..7f308ce --- /dev/null +++ b/tests/sync/test_yandex_smartcaptcha_image.py @@ -0,0 +1,23 @@ +"""Tests for YandexSmartCaptchaImageTask.""" + +from captcha_solver_api.tasks import YandexSmartCaptchaImageTask + + +class TestYandexSmartCaptchaImage: + def test_to_dict_uses_api_v2_task_type(self): + task = YandexSmartCaptchaImageTask( + image="captcha-image", + imgInstructions="instruction-image", + comment="click the symbols in the specified order", + ) + + assert task.to_dict() == { + "type": "SmartCaptchaTask", + "image": "captcha-image", + "imgInstructions": "instruction-image", + "comment": "click the symbols in the specified order", + } + + def test_optional_comment_is_omitted(self): + task = YandexSmartCaptchaImageTask("captcha-image", "instruction-image") + assert "comment" not in task.to_dict()