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
4 changes: 2 additions & 2 deletions .github/workflows/python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"] # asyncio isolated unittest work >= 3.8
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] # asyncio isolated unittest work >= 3.8

steps:
- uses: actions/checkout@v3
Expand All @@ -28,4 +28,4 @@ jobs:
pip install .
- name: Running tests
run: |
. run_tests.sh
. run_tests.sh
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ dist/
build/
env/
venv/
capmonstercloudclient.egg-info/
capmonstercloudclient.egg-info/
.mcp.json
.claude/
15 changes: 12 additions & 3 deletions capmonstercloud_client/CapMonsterCloudClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,16 @@
((BinanceTaskRequest,), getBinanceTimeouts),
((ImpervaCustomTaskRequest,), getImpervaTimeouts),
((RecognitionComplexImageTaskRequest,), getCITTimeouts),
((MTCaptchaRequest,), getImage2TextTimeouts),
((MTCaptchaRequest,), getMTCaptchaTimeouts),
((YidunRequest,), getYidunTimeouts),
((TemuCustomTaskRequest,), getTemuTimeouts),
((ProsopoTaskRequest,), getProsopoTimeouts),
((AltchaCustomTaskRequest,), getAltchaTimeouts),
((CastleCustomTaskRequest,), getCastleTimeouts),
((TspdCustomTaskRequest,), getTspdTimeouts),
((HuntCustomTaskRequest,), getHuntTimeouts),
((AlibabaCustomTaskRequest,), getAlibabaTimeouts),
((FriendlyCustomTaskRequest,), getFriendlyTimeouts),
)


Expand Down Expand Up @@ -106,7 +108,9 @@ async def solve_captcha(self, request: Union[
AltchaCustomTaskRequest,
CastleCustomTaskRequest,
TspdCustomTaskRequest,
HuntCustomTaskRequest],
HuntCustomTaskRequest,
AlibabaCustomTaskRequest,
FriendlyCustomTaskRequest],
) -> Dict[str, str]:
'''
Non-blocking method for captcha solving.
Expand Down Expand Up @@ -145,7 +149,12 @@ async def _solve(self, request: Union[
YidunRequest,
TemuCustomTaskRequest,
ProsopoTaskRequest,
AltchaCustomTaskRequest],
AltchaCustomTaskRequest,
CastleCustomTaskRequest,
TspdCustomTaskRequest,
HuntCustomTaskRequest,
AlibabaCustomTaskRequest,
FriendlyCustomTaskRequest],
timeouts: GetResultTimeouts,
) -> Dict[str, str]:

Expand Down
15 changes: 12 additions & 3 deletions capmonstercloud_client/GetResultTimeouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def getTurnstileTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 80)

def getDatadomeTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 80)
return GetResultTimeouts(1, 0, 3, 180)

def getTenDiTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 10, 3, 180)
Expand All @@ -51,7 +51,7 @@ def getBinanceTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 20)

def getImpervaTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 20)
return GetResultTimeouts(1, 0, 1, 15)

def getAltchaTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 50)
Expand All @@ -63,11 +63,20 @@ def getTspdTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 60)

def getHuntTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 1, 60)
return GetResultTimeouts(1, 0, 3, 180)

def getAlibabaTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 3, 120)

def getFriendlyTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 0, 3, 180)

def getCITTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(0.35, 0, 0.2, 10)

def getMTCaptchaTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 10, 3, 180)

def getYidunTimeouts() -> GetResultTimeouts:
return GetResultTimeouts(1, 10, 3, 180)

Expand Down
11 changes: 7 additions & 4 deletions capmonstercloud_client/clientOptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel, validator, Field
from pydantic import BaseModel, field_validator, Field
from .requests import ClientProxyInfo
from typing import Optional

Expand All @@ -10,19 +10,22 @@ class ClientOptions(BaseModel):
client_timeout: float = Field(default=20.0)


@validator('api_key')
@field_validator('api_key')
@classmethod
def validate_api_key(cls, value):
if not isinstance(value, str):
raise TypeError(f'Api Key must be <str> type, got {type(value)}')
return value

@validator('service_url')
@field_validator('service_url')
@classmethod
def validate_service_url(cls, value):
if not isinstance(value, str):
raise TypeError(f'Service url must be <str> type, got {type(value)}')
return value

@validator('default_soft_id')
@field_validator('default_soft_id')
@classmethod
def validate_soft_id(cls, value):
if not isinstance(value, int):
raise TypeError(f'Soft id must be <int> type, got {type(value)}')
Expand Down
59 changes: 59 additions & 0 deletions capmonstercloud_client/requests/AlibabaCustomTaskRequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from typing import Dict, Optional, Union
from pydantic import Field, field_validator

from .CustomTaskRequestBase import CustomTaskRequestBase

ALLOWED_METADATA_KEYS = {
'sceneId', 'prefix', 'userId', 'userUserId', 'verifyType',
'region', 'UserCertifyId', 'apiGetLib', 'cookieRequired',
}

class AlibabaCustomTaskRequest(CustomTaskRequestBase):
"""
Represents a payload structure for solving Alibaba custom captcha challenges.

Attributes:
captchaClass: The constant string value identifying the captcha
class as "alibaba".
metadata: A dictionary of Alibaba-specific parameters. Requires sceneId
and prefix; userId, userUserId, verifyType, region, UserCertifyId,
apiGetLib, and cookieRequired are optional, needed only for sites
that use them.
"""
captchaClass: str = Field(default='alibaba', description='The constant string value identifying the captcha class as "alibaba".')
metadata: Dict[str, Union[str, bool]] = Field(..., description='A dictionary of Alibaba-specific parameters. Requires sceneId and prefix; userId, userUserId, verifyType, region, UserCertifyId, apiGetLib, and cookieRequired are optional, needed only for sites that use them.')

@field_validator('metadata')
@classmethod
def validate_metadata(cls, value):
if not set(value.keys()).issubset(ALLOWED_METADATA_KEYS):
raise TypeError(f'Allowed keys for metadata are {sorted(ALLOWED_METADATA_KEYS)}')
if value.get('sceneId') is None:
raise TypeError(f'sceneId must be defined inside metadata.')
else:
if not isinstance(value.get('sceneId'), str):
raise TypeError(f'sceneId must be str.')
if value.get('prefix') is None:
raise TypeError(f'prefix must be defined inside metadata.')
else:
if not isinstance(value.get('prefix'), str):
raise TypeError(f'prefix must be str.')
if value.get('cookieRequired') is not None and not isinstance(value.get('cookieRequired'), bool):
raise TypeError(f'cookieRequired must be bool.')
return value

def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
task = {}
task['type'] = self.type
task['class'] = self.captchaClass
task['websiteURL'] = self.websiteUrl
task['metadata'] = self.metadata
if self.proxy:
task['proxyType'] = self.proxy.proxyType
task['proxyAddress'] = self.proxy.proxyAddress
task['proxyPort'] = self.proxy.proxyPort
task['proxyLogin'] = self.proxy.proxyLogin
task['proxyPassword'] = self.proxy.proxyPassword
if self.userAgent is not None:
task['userAgent'] = self.userAgent
return task
29 changes: 22 additions & 7 deletions capmonstercloud_client/requests/AltchaCustomTaskRequest.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
from typing import Dict, Union
from pydantic import Field, validator
from pydantic import Field, field_validator

from .CustomTaskRequestBase import CustomTaskRequestBase

class AltchaCustomTaskRequest(CustomTaskRequestBase):
captchaClass: str = Field(default='altcha')
websiteKey: str = Field()
metadata : Dict[str, str]
"""
Represents a payload structure for solving Altcha proof-of-work
captcha challenges via a custom module.

Attributes:
captchaClass: The class (subtype) identifier of the custom
module, constant "altcha" for this task.
websiteKey: The site key associated with the Altcha challenge
on the webpage. An empty string is allowed for this task.
metadata: A dictionary containing the Altcha challenge parameters
(challenge, iterations, salt, signature) extracted from the
webpage.
"""

captchaClass: str = Field(default='altcha', description='Class (subtype) identifier of the custom module, constant "altcha".')
websiteKey: str = Field(description='Site key associated with the Altcha challenge on the webpage. An empty string is allowed for this task.')
metadata: Dict[str, str] = Field(description='Altcha challenge parameters: challenge, iterations, salt, and signature.')

@validator('metadata')
@field_validator('metadata')
@classmethod
def validate_metadata(cls, value):
for key in ['challenge', 'iterations', 'salt', 'signature']:
if value.get(key) is None:
raise TypeError(f'Expect that {key} will be defined.')
raise TypeError(f'{key} must be defined inside metadata.')
else:
if not isinstance(value.get(key), str):
raise TypeError(f'Expect that {key} will be str.')
raise TypeError(f'{key} must be str.')
return value

def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
Expand Down
95 changes: 79 additions & 16 deletions capmonstercloud_client/requests/AmazonWafRequest.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,98 @@
from typing import Dict, Union, Optional
from pydantic import Field
from pydantic import Field, model_validator

from .baseRequestWithProxy import BaseRequestWithProxy

class AmazonWafRequest(BaseRequestWithProxy):
type: str = 'AmazonTask'
websiteUrl: str
challengeScript: str
captchaScript: str
websiteKey: str
context: str
iv: str
cookieSolution: Optional[bool] = Field(default=None)
"""
Represents a payload structure for solving Amazon AWS WAF captcha challenges.

The live API accepts three mutually exclusive parameter sets:
Option 1 (visible captcha): websiteUrl, websiteKey, captchaScript
required; challengeScript/context/iv must not be set.
Option 2 (challenge + captcha): websiteUrl, challengeScript, websiteKey,
context, iv required; captchaScript optional.
Option 3 (invisible/challenge-only): websiteUrl, challengeScript
required; context and iv must be empty strings; websiteKey and
captchaScript must not be set.

Attributes:
type: The constant string value identifying the task type as "AmazonTask".
websiteUrl: The URL of the webpage where the Amazon AWS WAF challenge
is presented.
challengeScript: Link to challenge.js served by AWS WAF on the target
page. Required for Options 2 and 3; must not be set for Option 1.
captchaScript: Link to captcha.js / jsapi.js served by AWS WAF on the
target page. Required for Option 1, optional for Option 2, must
not be set for Option 3.
websiteKey: The site key associated with the AWS WAF challenge.
Required for Options 1 and 2; must not be set for Option 3.
context: The context token provided by AWS WAF, used to correlate
the challenge with a specific session. Required for Option 2;
must be an empty string for Option 3; must not be set for Option 1.
iv: The initialization vector value provided by AWS WAF as part
of the challenge payload. Required for Option 2; must be an
empty string for Option 3; must not be set for Option 1.
cookieSolution: When set to True, requests the solution to be
returned as a ready-to-use cookie instead of a token.
userAgent: Browser User-Agent to emulate. Only used for Option 1.
Pass only a current Windows OS UA.
"""
type: str = Field(default='AmazonTask', description='The constant string value identifying the task type as "AmazonTask".')
websiteUrl: str = Field(..., description='The URL of the webpage where the Amazon AWS WAF challenge is presented.')
challengeScript: Optional[str] = Field(default=None, description='Link to challenge.js served by AWS WAF on the target page. Required for Options 2 and 3; must not be set for Option 1.')
captchaScript: Optional[str] = Field(default=None, description='Link to captcha.js / jsapi.js served by AWS WAF on the target page. Required for Option 1, optional for Option 2, must not be set for Option 3.')
websiteKey: Optional[str] = Field(default=None, description='The site key associated with the AWS WAF challenge. Required for Options 1 and 2; must not be set for Option 3.')
context: Optional[str] = Field(default=None, description='The context token provided by AWS WAF, used to correlate the challenge with a specific session. Required for Option 2; must be an empty string for Option 3; must not be set for Option 1.')
iv: Optional[str] = Field(default=None, description='The initialization vector value provided by AWS WAF as part of the challenge payload. Required for Option 2; must be an empty string for Option 3; must not be set for Option 1.')
cookieSolution: Optional[bool] = Field(default=None, description='When set to True, requests the solution to be returned as a ready-to-use cookie instead of a token.')
userAgent: Optional[str] = Field(default=None, description='Browser User-Agent to emulate. Only used for Option 1. Pass only a current Windows OS UA.')

@model_validator(mode='after')
def validate_amazon_waf_variant(self):
if self.challengeScript is None:
# Option 1: visible captcha, no challenge.js involved.
if self.websiteKey is None or self.captchaScript is None:
raise ValueError('"websiteKey" and "captchaScript" must be filled '
'when "challengeScript" is not provided (Option 1).')
if self.context is not None or self.iv is not None:
raise ValueError('"context" and "iv" are not used when "challengeScript" is not provided (Option 1).')
elif self.websiteKey is None and self.captchaScript is None:
# Option 3: invisible/challenge-only captcha.
if self.context != '' or self.iv != '':
raise ValueError('"context" and "iv" must be empty strings '
'when only "challengeScript" is provided (Option 3).')
else:
# Option 2: challenge + captcha.
if self.websiteKey is None or self.context is None or self.iv is None:
raise ValueError('"websiteKey", "context" and "iv" must be filled '
'when "challengeScript" is provided together with "websiteKey" (Option 2).')
return self

def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
task = {}
task['type'] = self.type
task['websiteURL'] = self.websiteUrl
task['challengeScript'] = self.challengeScript
task['captchaScript'] = self.captchaScript
task['websiteKey'] = self.websiteKey
task['context'] = self.context
task['iv'] = self.iv

if self.challengeScript is not None:
task['challengeScript'] = self.challengeScript
if self.captchaScript is not None:
task['captchaScript'] = self.captchaScript
if self.websiteKey is not None:
task['websiteKey'] = self.websiteKey
if self.context is not None:
task['context'] = self.context
if self.iv is not None:
task['iv'] = self.iv

if self.proxy:
task['proxyType'] = self.proxy.proxyType
task['proxyAddress'] = self.proxy.proxyAddress
task['proxyPort'] = self.proxy.proxyPort
task['proxyLogin'] = self.proxy.proxyLogin
task['proxyPassword'] = self.proxy.proxyPassword

if self.cookieSolution is not None:
task['cookieSolution'] = self.cookieSolution
if self.userAgent is not None:
task['userAgent'] = self.userAgent
return task
15 changes: 13 additions & 2 deletions capmonstercloud_client/requests/BasiliskCustomTaskRequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@
from .CustomTaskRequestBase import CustomTaskRequestBase

class BasiliskCustomTaskRequest(CustomTaskRequestBase):
captchaClass: str = Field(default='Basilisk')
websiteKey: str = Field()
"""
Represents a payload structure for solving Basilisk custom captcha
challenges.

Attributes:
captchaClass: The class (subtype) identifier of the custom module,
fixed to "Basilisk" for this task type.
websiteKey: The site key associated with the Basilisk captcha on
the webpage.
"""

captchaClass: str = Field(default='Basilisk', description='Class (subtype) identifier of the custom module, fixed to "Basilisk".')
websiteKey: str = Field(..., description='Site key associated with the Basilisk captcha on the webpage.')

def getTaskDict(self) -> Dict[str, Union[str, int, bool]]:
task = {}
Expand Down
Loading
Loading