diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..660cf96
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+__pycache__/
+build/
+dist/
+*.egg*/
\ No newline at end of file
diff --git a/README.md b/README.md
index 45dd102..a9bf13e 100644
--- a/README.md
+++ b/README.md
@@ -13,104 +13,154 @@ pip3 install captchatools
pip3 install -U captchatools
```
# How to use
-### Getting reCAPTCHA Tokens
+### Basic usage
```python
import captchatools
-solver = captchatools.captcha_harvesters(solving_site="capmonster", api_key="YOUR API KEY", sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", captcha_url="https://www.google.com/recaptcha/api2/demo")
+solver = captchatools.new_harvester(solving_site="capmonster", api_key="YOUR API KEY", sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", captcha_url="https://www.google.com/recaptcha/api2/demo")
captcha_answer = solver.get_token()
```
or
```python
-from captchatools import captcha_harvesters, exceptions
-solver = captcha_harvesters(solving_site=1, api_key="YOUR API KEY", sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", captcha_url="https://www.google.com/recaptcha/api2/demo")
+from captchatools import new_harvester
+solver = new_harvester(solving_site=1, api_key="YOUR API KEY", sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-", captcha_url="https://www.google.com/recaptcha/api2/demo")
captcha_answer = solver.get_token()
```
-
-### Getting Normal Captcha Tokens
-```python
-import captchatools
-solver = captchatools.captcha_harvesters(solving_site=2, captcha_type="normal", api_key="YOUR API KEY HERE")
-url = "https://www.scienceabc.com/wp-content/uploads/ext-www.scienceabc.com/wp-content/uploads/2016/07/Captcha-ex.jpg-.jpg"
-text_cap_answer = solver.get_normal(url)
-```
-
+### new_harvester() Parameters:
| Parameter | Required | Type | Default | Description|
| :-------------: |:-------------:| :-----:| :-----:| :-----:|
| api_key | true | String| -| The API Key for the captcha solving site|
-| solving_site| true| String (name of site) or int (site ID) | "capmonster"| Captcha solving site|
+| solving_site| true| String (name of site) or int (site ID) | "capmonster"| The captcha solving site that will be used. Refer to [the site IDs](https://github.com/Matthew17-21/Captcha-Tools/tree/main/captchatools-go#site-specific-support)|
| sitekey| true | String | - | Sitekey from the site where captcha is loaded|
| captcha_url | true| String | - | URL where the captcha is located|
-| captcha_type| false| String | "v2" | Type of captcha you are solving. Either captcha `v2`, `v3` or `hcaptcha` (`hcap` works aswell)|
+| captcha_type| false| String | "v2" | Type of captcha you are solving. Either captcha `image`, `v2`, `v3` or `hcaptcha` (`hcap` works aswell)|
| invisible_captcha| false | bool | false | If the captcha is invisible or not.
__This param is only required when solving invisible captchas__|
| min_score | false | double |0.7 | Minimum score for v3 captchas.
__This param is only required when solving V3 and it needs a higher / lower score__|
| action | false | String | "verify" | Action that is associated with the V3 captcha.
__This param is only required when solving V3 captchas__|
-| soft_id | false | int | 4782723 |2captcha Developer ID.
Developers get 10% of spendings of their software users. |
+| soft_id | false | int | - |2captcha Developer ID.
Developers get 10% of spendings of their software users. |
+### get_token() Parameters:
+| Field | Required | Type | Description|
+| :-------------: |:-------------:| :-----:| :-----:|
+| b64_img | false | string | Base64 encoded captcha image
__This param is only required when solving image captchas__|
+| proxy| false | string | Proxy to be used to solve captchas.
This will make the captcha be solved from the proxy ip
Format: `ip:port:user:pass` |
+| proxy_type | false | string | Type of the proxy being used. Options are:
`HTTP`, `HTTPS`, `SOCKS4`, `SOCKS5`|
+| user_agent | false | string | UserAgent that will be passed to the service and used to solve the captcha |
+### Examples
+##### Example - V2 Captcha / Basic usage
+```python
+from captchatools import new_harvester
+
+def main():
+ harvester = new_harvester(
+ api_key="CHANGE THIS",
+ solving_site="capsolver",
+ captcha_type="v2",
+ sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
+ captcha_url="https://www.google.com/recaptcha/api2/demo"
+ )
+ answer = harvester.get_token()
+```
+##### Example - V3 Captcha
+```python
+from captchatools import new_harvester
+
+def main():
+ harvester = new_harvester(
+
+ api_key="CHANGE THIS",
+ solving_site="capsolver",
+
+ captcha_type="v3",
+ sitekey="6LcR_okUAAAAAPYrPe-HK_0RULO1aZM15ENyM-Mf",
+ captcha_url="https://antcpt.com/score_detector/",
+ action="homepage",
+ min_score=0.7
+ )
+ token = harvester.get_token()
+```
+##### Example - Image captcha
+```python
+from captchatools import new_harvester
+
+def main():
+ harvester = new_harvester(
+ api_key="CHANGE THIS",
+ solving_site="capsolver",
+ captcha_type="image",
+ )
+ token = harvester.get_token(b64_img="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")
+```
+##### Example - Additional captcha data
+```python
+from captchatools import new_harvester
+
+def main():
+ harvester = new_harvester(
+ api_key="CHANGE THIS",
+ solving_site="capsolver",
+ captcha_type="v2",
+ sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
+ captcha_url="https://www.google.com/recaptcha/api2/demo"
+ )
+
+ token = harvester.get_token(
+ proxy="ip:port:user:pass",
+ proxy_type="http",
+ user_agent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
+ )
+```
# Supported Sites
- **[Capmonster](https://capmonster.cloud/)**
- **[2Captcha](https://www.2captcha.com/)**
- **[Anticaptcha](https://www.anti-captcha.com/)**
+- **[Capsolver](https://www.capsolver.com/)**
+
+### Site-Specific Support:
+| Site | Site ID |Captcha Types Supported | Task Types Supported|
+| :-------------: |:-------------:| :-----:| :-----:|
+| Capmonster | captchatools.CapmonsterSite| Image captchas,
Recaptcha V2,
Recaptcha V3,
HCaptcha | ImageToTextTask,
NoCaptchaTask,
NoCaptchaTaskProxyless,
RecaptchaV3TaskProxyless,
HCaptchaTaskProxyless |
+| Anticaptcha | captchatools.AnticaptchaSite| Image captchas,
Recaptcha V2,
Recaptcha V3,
HCaptcha | ImageToTextTask,
RecaptchaV2Task
RecaptchaV2TaskProxyless,
RecaptchaV3TaskProxyless,
HCaptchaTaskProxyless |
+| 2Captcha | captchatools.TwoCaptchaSite| Image captchas,
Recaptcha V2,
Recaptcha V3,
HCaptcha | - |
-##### Site-Specific Support:
-| Site |Site ID| Captcha Types Supported | Task Types Supported|
-| :-------------: |:-------------:|:-------------:| :-----:|
-| Capmonster |1| Recaptcha V2,
Recaptcha V3,
HCaptcha | RecaptchaV2TaskProxyless,
RecaptchaV3TaskProxyless,
HCaptchaTaskProxyless,
ImageToTextTask
|
-| Anticaptcha |2| Recaptcha V2,
Recaptcha V3,
HCaptcha | RecaptchaV2TaskProxyless,
RecaptchaV3TaskProxyless,
HCaptchaTaskProxyless
ImageToTextTask
|
-| 2Captcha |3| Recaptcha V2,
Recaptcha V3,
HCaptcha | - |
# Recommendations
1. For 2Captcha, don't run more than 60 tasks per API key.
-2. Handle exceptions appropriately.
- * If a `NoBalanceException` is thrown, tasks should stop. Some sites will temporarily ban IP's if constant requests come in.
+2. Handle errors appropriately.
+ * If a `ErrNoBalance` is thrown, tasks should stop. Some sites will temporarily ban IP's if constant requests come in.
-# Exceptions
-| Exception | Raised |
+# Errors
+| Errors | Returned When |
| :--------:| :-----:|
-| `NoBalanceException` | Balance is below 0 for captcha solving site|
-| `WrongAPIKeyExceptionException` | Incorrect API Key for captcha solving site|
-| `WrongSitekeyException` | Incorrect sitekey |
-| `NoHarvesterException` | When the user did not / incorrectly chose a captcha harvester. Refer to the [guide](https://github.com/Matthew17-21/Captcha-Tools#how-to-use) |
-| `CaptchaIMGTooBig` | The size of the captcha image is too big for the solving service. |
-| `FailedToGetCapIMG`| Failed to get the captcha image from the URL.
**Tries 3 times before getting thrown.**
+| `ErrNoBalance` | Balance is below 0 for captcha solving site|
+| `ErrWrongAPIKey` | Incorrect API Key for captcha solving site|
+| `ErrWrongSitekey` | Incorrect sitekey |
+| `ErrIncorrectCapType` | Incorrectly chose a captcha type. When initializing a new harvester. Refer to [the captcha types](https://github.com/Matthew17-21/Captcha-Tools/tree/main/captchatools-go#config-struct-fields) |
+| `ErrNoHarvester` | When the user did not / incorrectly chose a captcha harvester. Refer to the ["how to use" guide](https://github.com/Matthew17-21/Captcha-Tools/tree/main/captchatools-go#how-to-use) |
+##### Error Handling
```python
-from captchatools import captcha_harvesters, exceptions as captchaExceptions
-try:
- ...
-except captchaExceptions.NoBalanceException:
- print("No balance.")
-```
-or
-```python
-import captchatools
-try:
- ...
-except captchatools.NoBalanceException:
- print("No balance.")
+from captchatools import new_harvester, exceptions as captchaExceptions,
+
+def main():
+ try:
+ harvester = new_harvester()
+ token harvester.get_token()
+ except captchaExceptions.NoHarvesterException:
+ print("I need to set my captcha harvester!")
```
-# TO DO
-1. [] Document code better
-2. [] 2Captcha
- * [] Clean up code
- * [] Proxy support
- * [] Cookie support
- * [] User Agent Support
- * [] Different type of captchas
-3. [] Anticaptcha
- * [] Clean up code
- * [] Proxy support
- * [] Cookie support
- * [] User Agent Support
- * [] Different type of captchas
-4. [] Capmonster
- * [] Clean up code
- * [] Proxy support
- * [] Cookie support
- * [] User Agent Support
- * [] Different type of captchas
-5. [] Add DeathByCaptcha
-6. [] Release in Go
-7. [] Allow for refunds
\ No newline at end of file
+
+# Changelog
+### 1.3.0
+##### What's new
+1. Get Balance Support
+2. Proxy Support
+3. User Agent Support
+4. Text image captcha support
+5. Better internal handling
+6. Capsolver support
+
+##### Important Changes
+* It is recommend to use the `new_harvester` function rather than the old `captcha_harvesters`
\ No newline at end of file
diff --git a/captchatools/__init__.py b/captchatools/__init__.py
index e4ae0e2..c24f399 100644
--- a/captchatools/__init__.py
+++ b/captchatools/__init__.py
@@ -16,7 +16,75 @@
2 = Anticaptcha
3 = 2captcha
'''
-from .harvesters import captcha_harvesters
-from .exceptions import(
- WrongAPIKeyException, NoBalanceException, WrongSitekeyException, NoHarvesterException
-)
\ No newline at end of file
+__version__ = "2.0.0"
+__author__ = "Matthew17-21"
+__license__ = "MIT"
+
+from abc import ABC, abstractmethod
+from typing import Optional
+from . import exceptions as captchaExceptions
+class Harvester(ABC):
+ '''
+ Represents a captcha harvester.
+ '''
+ def __init__(self, **kwargs) -> None:
+ self.api_key = kwargs.get("api_key",None)
+ self.captcha_type = kwargs.get("captcha_type","v2").lower()
+ self.solving_site = kwargs.get("solving_site",None)
+ self.invisible_captcha = kwargs.get("invisible_captcha",False)
+ self.captcha_url = kwargs.get("captcha_url",None)
+ self.min_score = kwargs.get("min_score",0.7)
+ self.sitekey = kwargs.get("sitekey",None)
+ self.action = kwargs.get("action","verify")
+ self.soft_id = kwargs.get("soft_id",None)
+
+ # Validate Data
+ if self.api_key is None:
+ raise captchaExceptions.WrongAPIKeyException()
+ if self.solving_site is None:
+ raise captchaExceptions.NoHarvesterException("No solving site selected")
+ if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap", "image", "normal"]:
+ raise captchaExceptions.NoCaptchaType("Invalid captcha type")
+ if self.soft_id is None:
+ if self.solving_site == 3 or self.solving_site == "2captcha":
+ self.soft_id = 4782723
+
+ @abstractmethod
+ def get_balance(self) -> float:
+ '''
+ Returns the balance for the current captcha harvster
+ '''
+
+ @abstractmethod
+ def get_token(
+ self, b64_img: Optional[str]=None,
+ user_agent: Optional[str]=None,
+ proxy: Optional[str]=None,
+ proxy_type: Optional[str]=None
+ ):
+ '''
+ Returns a captcha token
+ '''
+
+def new_harvester(**kwargs) -> Harvester:
+ # Need to import here to prevent circular imports
+ from .twocap import Twocap
+ from .anticaptcha import Anticaptcha
+ from .capmonster import Capmonster
+ from .capsolver import Capsolver
+
+ site = kwargs.get("solving_site","").lower()
+ if site == 1 or site == "capmonster":
+ return Capmonster(**kwargs)
+ elif site == 2 or site == "anticaptcha":
+ return Anticaptcha(**kwargs)
+ elif site == 3 or site == "2captcha":
+ return Twocap(**kwargs)
+ elif site == 4 or site == "capsolver":
+ return Capsolver(**kwargs)
+ raise captchaExceptions.NoHarvesterException("No solving site selected")
+
+
+# Just for backward compatibility
+def captcha_harvesters(**kwargs) -> Harvester:
+ return new_harvester(**kwargs)
\ No newline at end of file
diff --git a/captchatools/__init__.pyi b/captchatools/__init__.pyi
new file mode 100644
index 0000000..413fe1f
--- /dev/null
+++ b/captchatools/__init__.pyi
@@ -0,0 +1,54 @@
+from abc import ABC, abstractmethod
+from typing import Optional
+class Harvester(ABC):
+ '''
+ Represents a captcha harvester.
+ '''
+ def __init__(
+ self,
+ api_key: Optional[str] = None,
+ captcha_type: Optional[str] = "v2",
+ solving_site: Optional[str] = None,
+ invisible_captcha: Optional[bool] = False,
+ captcha_url: Optional[str] = None,
+ min_score : Optional[float] = 0.7,
+ sitekey : Optional[str] = None,
+ action: Optional[str] = "verify",
+ soft_id: Optional[str] = None
+ ) -> None: ...
+
+ @abstractmethod
+ def get_balance(self) -> float: ...
+
+ @abstractmethod
+ def get_token(
+ self, b64_img: Optional[str]=None,
+ user_agent: Optional[str]=None,
+ proxy: Optional[str]=None,
+ proxy_type: Optional[str]=None
+ ): ...
+
+def new_harvester(
+ api_key: Optional[str] = None,
+ captcha_type: Optional[str] = "v2",
+ solving_site: Optional[str] = None,
+ invisible_captcha: Optional[bool] = False,
+ captcha_url: Optional[str] = None,
+ min_score : Optional[float] = 0.7,
+ sitekey : Optional[str] = None,
+ action: Optional[str] = "verify",
+ soft_id: Optional[str] = None
+) -> Harvester: ...
+
+# Just for backward compatibility
+def captcha_harvesters(
+ api_key: Optional[str] = None,
+ captcha_type: Optional[str] = "v2",
+ solving_site: Optional[str] = None,
+ invisible_captcha: Optional[bool] = False,
+ captcha_url: Optional[str] = None,
+ min_score : Optional[float] = 0.7,
+ sitekey : Optional[str] = None,
+ action: Optional[str] = "verify",
+ soft_id: Optional[str] = None
+) -> Harvester: ...
\ No newline at end of file
diff --git a/captchatools/anticaptcha.py b/captchatools/anticaptcha.py
index bfd4b60..cdfa1c5 100644
--- a/captchatools/anticaptcha.py
+++ b/captchatools/anticaptcha.py
@@ -1,106 +1,131 @@
-import requests
+from . import Harvester
from . import exceptions as captchaExceptions
+import requests
+from typing import Optional
import time
BASEURL = "https://api.anti-captcha.com"
+class Anticaptcha(Harvester):
+ def get_balance(self) -> float:
+ payload = {"clientKey": self.api_key}
+ for _ in range(5):
+ try:
+ resp = requests.post("https://api.anti-captcha.com/getBalance", json=payload ,timeout=20).json()
+ if resp["errorId"] == 1: # Means there was an error
+ self.check_error(resp["errorCode"])
+ return float(resp["balance"])
+ except requests.RequestException:
+ pass
-# There's others way we could've done this,
-# but it's sufficient for right now.
-class Anticap:
- '''
- This object will contain the data to interact with anticaptcha.com API
- '''
- def __init__(self, parent:object):
- self.user_data = parent
-
- def get_token(self) -> str:
- return self.get_answer( self.get_id() )
+ def get_token(self, b64_img: Optional[str] = None, user_agent: Optional[str] = None, proxy: Optional[str] = None, proxy_type: Optional[str] = "HTTP"):
+ # Get ID
+ task_id = self.__get_id(
+ b64_img=b64_img,
+ user_agent=user_agent,
+ proxy=proxy,
+ proxy_type=proxy_type
+ )
+
+ # Get Answer
+ return self.__get_answer(task_id)
- def get_normal(self, cap_pic_url) -> str:
- return self.get_answer(self.get_id(cap_pic_url), True)
-
- def get_id(self, cap_pic_url=None) -> int:
- '''
- Method to get Queue ID from the API.
- '''
+ def __create_payload(self, **kwargs):
+ payload = {"clientKey":self.api_key,"task":{}}
- # Define the payload we are going to send to the API
- payload = {
- "clientKey":self.user_data.api_key,
- "task":{
- "websiteURL":self.user_data.captcha_url,
- "websiteKey": self.user_data.sitekey
- }
- }
-
- # Add anything else to the API, based off the user's input
- if self.user_data.captcha_type == "v2":
- payload["task"]["type"] = "NoCaptchaTaskProxyless"
- if self.user_data.invisible_captcha:
- payload["task"]["isInvisible"] = True
-
- elif self.user_data.captcha_type == "v3":
+ # Add data based on the captcha type
+ if self.captcha_type == "image" or self.captcha_type == "normal":
+ payload["task"]["type"] = "ImageToTextTask"
+ payload["task"]["body"] = kwargs.get("b64_img", "")
+ elif self.captcha_type == "v2":
+ payload["task"]["type"] = "RecaptchaV2TaskProxyless"
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+ if self.invisible_captcha:
+ payload["task"]["isInvisible"] = self.invisible_captcha
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "RecaptchaV2Task"
+ elif self.captcha_type == "v3":
payload["task"]["type"] = "RecaptchaV3TaskProxyless"
- payload["task"]["minScore"] = self.user_data.min_score
- payload["task"]["pageAction"] = self.user_data.action
-
- elif self.user_data.captcha_type == "hcap" or self.user_data.captcha_type == "hcaptcha":
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+ payload["task"]["minScore"] = self.min_score
+ payload["task"]["pageAction"] = self.action
+ elif self.captcha_type == "hcap" or self.captcha_type == "hcaptcha":
payload["task"]["type"] = "HCaptchaTaskProxyless"
-
- elif self.user_data.captcha_type == "normal":
- payload["task"]["type"] = "ImageToTextTask"
- payload["task"]["body"] = self.user_data.get_cap_img(cap_pic_url)
-
- # Get the Queue ID b sending a POST request to their API
- while True:
- try:
- resp = requests.post(BASEURL + "/createTask ", json=payload).json()
- if resp["errorId"] == 0: # Means there was no error
- return resp["taskId"] # Return the queue ID
-
- elif resp["errorCode"] == "ERROR_ZERO_BALANCE":
- raise captchaExceptions.NoBalanceException()
-
- elif resp["errorCode"] == "ERROR_RECAPTCHA_INVALID_SITEKEY":
- raise captchaExceptions.WrongSitekeyException()
-
- elif resp["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST":
- # Throw Exception
- raise captchaExceptions.WrongAPIKeyException()
-
- elif resp["errorCode"] == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
- raise captchaExceptions.CaptchaIMGTooBig()
- except requests.RequestException:
- pass
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "HCaptchaTask"
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
- def get_answer(self, queue_id, isTextCap=False) -> str:
- '''
- This method gets the captcha token from the API
- '''
- # Get the captcha token from their API
- while True:
+ # Add Global Data
+ if self.soft_id is not None:
+ payload["softId"] = self.soft_id
+ if kwargs.get("proxy", None) is not None:
+ splitted = kwargs.get("proxy").split(":")
+ payload["task"]["proxyType"] = kwargs.get('proxy_type',"HTTP")
+ payload["task"]["proxyAddress"] = splitted[0]
try:
- payload_getAnswer = {
- "clientKey":self.user_data.api_key,
- "taskId": queue_id
- }
-
- answer = requests.post(BASEURL + "/getTaskResult", json=payload_getAnswer).json()
- if answer["status"] == "ready" and not isTextCap:
- return answer["solution"]["gRecaptchaResponse"]
- elif answer["status"] == "ready" and isTextCap:
- return answer["solution"]["text"]
-
- elif answer["errorId"] == 12 or answer["errorId"] == 16:
- # Captcha unsolvable || TaskID doesn't exist
- # Restart the entire program
- self.get_token()
-
-
- time.sleep(4)
- except KeyError:
- self.get_token()
+ payload["task"]["proxyPort"] = int(splitted[1])
except Exception:
- pass
\ No newline at end of file
+ payload["task"]["proxyPort"] = splitted[1]
+ if len(splitted) >=4:
+ payload["task"]["proxyLogin"] = splitted[2]
+ payload["task"]["proxyPassword"] = splitted[3]
+ if kwargs.get("user_agent", None) is not None:
+ payload["task"]["userAgent"] = kwargs.get("user_agent")
+ return payload
+
+ def __get_id(self,**kwargs):
+ # Create Payload
+ payload = self.__create_payload(**kwargs)
+
+ # Get token & return it
+ for _ in range(50):
+ try:
+ resp = requests.post(BASEURL + "/createTask " , json=payload, timeout=20).json()
+ if resp["errorId"] != 0: # Means there was an error:
+ self.check_error(resp["errorCode"])
+ return resp["taskId"]
+ except (requests.RequestException, KeyError):
+ pass
+
+ def __get_answer(self,task_id:int):
+ payload = {"clientKey":self.api_key,"taskId": task_id}
+ for _ in range(100):
+ try:
+ response = requests.post(BASEURL + "/getTaskResult",json=payload,timeout=20,).json()
+ if response["errorId"] != 0: # Means there was an error
+ self.check_error(response["errorId"])
+ if response["status"] == "processing":
+ time.sleep(3)
+ continue
+ if self.captcha_type == "normal" or self.captcha_type == "image":
+ return response["solution"]["text"]
+ else:
+ return response["solution"]["gRecaptchaResponse"]
+ except (requests.RequestException, KeyError):
+ pass
+
+ @staticmethod
+ def check_error(error_code):
+ if error_code == "ERROR_ZERO_BALANCE":
+ raise captchaExceptions.NoBalanceException()
+ elif error_code == "ERROR_WRONG_GOOGLEKEY":
+ raise captchaExceptions.WrongSitekeyException()
+ elif error_code == "ERROR_WRONG_USER_KEY" or error_code == "ERROR_KEY_DOES_NOT_EXIST":
+ raise captchaExceptions.WrongAPIKeyException()
+ elif error_code == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
+ raise captchaExceptions.CaptchaIMGTooBig()
+ elif error_code == "ERROR_PAGEURL":
+ raise captchaExceptions.TaskDetails(f"Error: {error_code}")
+ elif error_code == "MAX_USER_TURN" or error_code == "ERROR_NO_SLOT_AVAILABLE":
+ raise captchaExceptions.NoSlotAvailable("No slot available")
+ elif error_code == "ERROR_IP_NOT_ALLOWED" or error_code == "IP_BANNED" or error_code == "ERROR_IP_BLOCKED":
+ return captchaExceptions.Banned(error_code)
+ elif error_code == "ERROR_ZERO_CAPTCHA_FILESIZE" or error_code == "ERROR_UPLOAD" or \
+ error_code == "ERROR_CAPTCHAIMAGE_BLOCKED" or error_code == "ERROR_IMAGE_TYPE_NOT_SUPPORTED" or \
+ error_code == "ERROR_WRONG_FILE_EXTENSION":
+ raise captchaExceptions.CaptchaImageError(error_code)
+ else: raise captchaExceptions.UnknownError(f"Error returned from 2captcha: {error_code}")
diff --git a/captchatools/capmonster.py b/captchatools/capmonster.py
index 8ef4058..eed930f 100644
--- a/captchatools/capmonster.py
+++ b/captchatools/capmonster.py
@@ -1,104 +1,136 @@
-import requests
from . import exceptions as captchaExceptions
+from . import Harvester
import time
+import requests
+from typing import Optional
BASEURL = "https://api.capmonster.cloud"
-# There's others way we could've done this,
-# but it's sufficient for right now.
-class Capmonster:
+
+class Capmonster(Harvester):
'''
This object will contain the data to interact with capmonster.cloud API
'''
- def __init__(self, parent:object):
- self.user_data = parent
+ def get_balance(self) -> float:
+ payload = {"clientKey": self.api_key}
+ for _ in range(5):
+ try:
+ resp = requests.post(BASEURL + "/getBalance", json=payload ,timeout=20).json()
+ if resp["errorId"] == 1: # Means there was an error
+ self.check_error(resp["errorCode"])
+ return float(resp["balance"])
+ except requests.RequestException:
+ pass
- def get_token(self) -> str:
- return self.get_answer( self.get_id() )
+ def get_token(self, b64_img: Optional[str] = None, user_agent: Optional[str] = None, proxy: Optional[str] = None, proxy_type: Optional[str] = "HTTP"):
+ # Get ID
+ task_id = self.__get_id(
+ b64_img=b64_img,
+ user_agent=user_agent,
+ proxy=proxy,
+ proxy_type=proxy_type
+ )
+
+ # Get Answer
+ return self.__get_answer(task_id)
- def get_normal(self, path_to_img) -> str:
- return self.get_answer(self.get_id(path_to_img), True)
+ def __create_payload(self, **kwargs):
+ payload = {"clientKey":self.api_key,"task":{}}
- def get_id(self, cap_pic_url=None) -> int:
- '''
- Method to get Queue ID from the API.
- '''
-
- # Define the payload we are going to send to the API
- payload = {
- "clientKey":self.user_data.api_key,
- "task":{
- "websiteURL":self.user_data.captcha_url,
- "websiteKey": self.user_data.sitekey
- }
- }
-
- # Add anything else to the API, based off the user's input
- if self.user_data.captcha_type == "v2":
+ # Add data based on the captcha type
+ if self.captcha_type == "image" or self.captcha_type == "normal":
+ payload["task"]["type"] = "ImageToTextTask"
+ payload["task"]["body"] = kwargs.get("b64_img", "")
+ elif self.captcha_type == "v2":
payload["task"]["type"] = "NoCaptchaTaskProxyless"
- if self.user_data.invisible_captcha:
- payload["task"]["isInvisible"] = True
-
- elif self.user_data.captcha_type == "v3":
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+ if self.invisible_captcha:
+ payload["task"]["isInvisible"] = self.invisible_captcha
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "NoCaptchaTask"
+ elif self.captcha_type == "v3":
payload["task"]["type"] = "RecaptchaV3TaskProxyless"
- payload["task"]["minScore"] = self.user_data.min_score
- payload["task"]["pageAction"] = self.user_data.action
- elif self.user_data.captcha_type == "hcap" or self.user_data.captcha_type == "hcaptcha":
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+ payload["task"]["minScore"] = self.min_score
+ payload["task"]["pageAction"] = self.action
+ elif self.captcha_type == "hcap" or self.captcha_type == "hcaptcha":
payload["task"]["type"] = "HCaptchaTaskProxyless"
-
- elif self.user_data.captcha_type == "normal":
- payload["task"]["type"] = "ImageToTextTask"
- payload["task"]["body"] = self.user_data.get_cap_img(cap_pic_url)
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "HCaptchaTask"
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
- # Get the Queue ID b sending a POST request to their API
- while True:
+ # Add Global Data
+ if self.soft_id is not None:
+ payload["softId"] = self.soft_id
+ if kwargs.get("proxy", None) is not None:
+ splitted = kwargs.get("proxy").split(":")
+ payload["task"]["proxyAddress"] = splitted[0]
try:
- resp = requests.post(BASEURL + "/createTask", json=payload).json()
- if resp["errorId"] == 0: # Means there was no error
- return resp["taskId"] # Return the queue ID
-
- elif resp["errorCode"] == "ERROR_ZERO_BALANCE":
- # Throw Exception
- raise captchaExceptions.NoBalanceException()
-
- elif resp["errorCode"] == "ERROR_RECAPTCHA_INVALID_SITEKEY":
- # Throw Exception
- raise captchaExceptions.WrongSitekeyException()
-
- elif resp["errorCode"] == "ERROR_KEY_DOES_NOT_EXIST":
- # Throw Exception
- raise captchaExceptions.WrongAPIKeyException()
-
- elif resp["errorCode"] == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
- raise captchaExceptions.CaptchaIMGTooBig()
- except requests.RequestException:
+ payload["task"]["proxyPort"] = int(splitted[1])
+ except Exception:
+ payload["task"]["proxyPort"] = splitted[1]
+ if len(splitted) >=4:
+ payload["task"]["proxyLogin"] = splitted[2]
+ payload["task"]["proxyPassword"] = splitted[3]
+ payload["task"]["proxyType"] = kwargs.get("proxy_type", "http")
+ if kwargs.get("user_agent", None) is not None:
+ payload["task"]["userAgent"] = kwargs.get("user_agent")
+ return payload
+
+ def __get_id(self,**kwargs):
+ # Create Payload
+ payload = self.__create_payload(**kwargs)
+
+ # Get token & return it
+ for _ in range(50):
+ try:
+ resp = requests.post(BASEURL + "/createTask" , json=payload, timeout=20).json()
+ if resp["errorId"] != 0: # Means there was an error:
+ self.check_error(resp["errorCode"])
+ return resp["taskId"]
+ except (requests.RequestException, KeyError):
pass
-
- def get_answer(self, queue_id, isTextCap=False) -> str:
- '''
- This method gets the captcha token from the API
- '''
- payload_getAnswer = {
- "clientKey":self.user_data.api_key,
- "taskId": queue_id
- }
- while True:
+
+ def __get_answer(self,task_id:int):
+ payload = {"clientKey":self.api_key,"taskId": task_id}
+ for _ in range(100):
try:
- answer = requests.post(BASEURL + "/getTaskResult", json=payload_getAnswer).json()
- if answer["status"] == "ready" and not isTextCap:
- return answer["solution"]["gRecaptchaResponse"]
- elif answer["status"] == "ready" and isTextCap:
- return answer["solution"]["text"]
-
- elif answer["errorId"] == 12 or answer["errorId"] == 16:
- # Captcha unsolvable || TaskID doesn't exist
- self.get_token()
-
-
- time.sleep(4)
- except KeyError:
- self.get_token()
- except Exception:
- pass
\ No newline at end of file
+ response = requests.post(BASEURL + "/getTaskResult",json=payload,timeout=20,).json()
+ if response["errorId"] != 0: # Means there was an error
+ self.check_error(response["errorCode"])
+ if response["status"] == "processing":
+ time.sleep(3)
+ continue
+ if self.captcha_type == "normal" or self.captcha_type == "image":
+ return response["solution"]["text"]
+ else:
+ return response["solution"]["gRecaptchaResponse"]
+ except (requests.RequestException, KeyError):
+ pass
+
+ @staticmethod
+ def check_error(error_code):
+ if error_code == "ERROR_ZERO_BALANCE":
+ raise captchaExceptions.NoBalanceException()
+ elif error_code == "ERROR_WRONG_GOOGLEKEY":
+ raise captchaExceptions.WrongSitekeyException()
+ elif error_code == "ERROR_WRONG_USER_KEY" or error_code == "ERROR_KEY_DOES_NOT_EXIST":
+ raise captchaExceptions.WrongAPIKeyException()
+ elif error_code == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
+ raise captchaExceptions.CaptchaIMGTooBig()
+ elif error_code == "ERROR_PAGEURL":
+ raise captchaExceptions.TaskDetails(f"Error: {error_code}")
+ elif error_code == "MAX_USER_TURN" or error_code == "ERROR_NO_SLOT_AVAILABLE":
+ raise captchaExceptions.NoSlotAvailable("No slot available")
+ elif error_code == "ERROR_IP_NOT_ALLOWED" or error_code == "IP_BANNED" or error_code == "ERROR_IP_BLOCKED":
+ return captchaExceptions.Banned(error_code)
+ elif error_code == "ERROR_ZERO_CAPTCHA_FILESIZE" or error_code == "ERROR_UPLOAD" or \
+ error_code == "ERROR_CAPTCHAIMAGE_BLOCKED" or error_code == "ERROR_IMAGE_TYPE_NOT_SUPPORTED" or \
+ error_code == "ERROR_WRONG_FILE_EXTENSION":
+ raise captchaExceptions.CaptchaImageError(error_code)
+ else: raise captchaExceptions.UnknownError(f"Error returned from anticaptcha: {error_code}")
\ No newline at end of file
diff --git a/captchatools/capsolver.py b/captchatools/capsolver.py
new file mode 100644
index 0000000..43d1bda
--- /dev/null
+++ b/captchatools/capsolver.py
@@ -0,0 +1,156 @@
+from . import exceptions as captchaExceptions
+from . import Harvester
+import time
+import requests
+from typing import Optional
+
+BASEURL = "https://api.capsolver.com"
+
+
+
+class Capsolver(Harvester):
+ '''
+ This object will contain the data to interact with Capsolver API
+ '''
+ def get_balance(self) -> float:
+ payload = {"clientKey": self.api_key}
+ for _ in range(5):
+ try:
+ resp = requests.post(BASEURL + "/getBalance", json=payload ,timeout=20).json()
+ if resp["errorId"] == 1: # Means there was an error
+ self.check_error(resp["errorCode"])
+ return float(resp["balance"])
+ except requests.RequestException:
+ pass
+
+ def get_token(self, b64_img: Optional[str] = None, user_agent: Optional[str] = None, proxy: Optional[str] = None, proxy_type: Optional[str] = "HTTP"):
+ # Get ID
+ task_id, already_solved = self.__get_id(
+ b64_img=b64_img,
+ user_agent=user_agent,
+ proxy=proxy,
+ proxy_type=proxy_type
+ )
+
+ # Check if token was already retrieved
+ if already_solved:
+ return task_id
+
+ # Get Answer
+ return self.__get_answer(task_id)
+
+ def __create_payload(self, **kwargs):
+ payload = {"clientKey":self.api_key,"task":{}}
+
+ # Add data based on the captcha type
+ if self.captcha_type == "image" or self.captcha_type == "normal":
+ payload["task"]["type"] = "ImageToTextTask"
+ payload["task"]["body"] = kwargs.get("b64_img", "")
+ elif self.captcha_type == "v2":
+ payload["task"]["type"] = "ReCaptchaV2TaskProxyLess"
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+ if self.invisible_captcha:
+ payload["task"]["isInvisible"] = self.invisible_captcha
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "ReCaptchaV2Task"
+ elif self.captcha_type == "v3":
+ payload["task"]["type"] = "RecaptchaV3TaskProxyless"
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+ payload["task"]["minScore"] = self.min_score
+ payload["task"]["pageAction"] = self.action
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "ReCaptchaV3Task"
+ elif self.captcha_type == "hcap" or self.captcha_type == "hcaptcha":
+ payload["task"]["type"] = "HCaptchaTaskProxyless"
+ if kwargs.get("proxy", None) is not None:
+ payload["task"]["type"] = "HCaptchaTask"
+ payload["task"]["websiteURL"] = self.captcha_url
+ payload["task"]["websiteKey"] = self.sitekey
+
+
+ # Add Global Data
+ if self.soft_id is not None:
+ payload["appId"] = self.soft_id
+ if kwargs.get("proxy", None) is not None:
+ splitted = kwargs.get("proxy").split(":")
+ payload["task"]["proxyAddress"] = splitted[0]
+ try:
+ payload["task"]["proxyPort"] = int(splitted[1])
+ except Exception:
+ payload["task"]["proxyPort"] = splitted[1]
+ if len(splitted) >=4:
+ payload["task"]["proxyLogin"] = splitted[2]
+ payload["task"]["proxyPassword"] = splitted[3]
+ payload["task"]["proxyType"] = kwargs.get("proxy_type", "http")
+ if kwargs.get("user_agent", None) is not None:
+ payload["task"]["userAgent"] = kwargs.get("user_agent")
+ return payload
+
+ def __get_id(self,**kwargs):
+ '''
+ This method gets a task ID for a captcha token.
+
+ A tuple is returned from this method ( Optional[task_id|captcha_token] | bool)
+ If the second index is True, that means a captcha token is returned instead of a task ID
+ '''
+ # Create Payload
+ payload = self.__create_payload(**kwargs)
+
+ # Get token & return it
+ for _ in range(50):
+ try:
+ resp = requests.post(BASEURL + "/createTask" , json=payload, timeout=20).json()
+ if resp["errorId"] != 0: # Means there was an error:
+ self.check_error(resp["errorCode"])
+
+ # Check if there is an answer already available
+ if resp["status"] == "ready":
+ if resp["solution"].get("text", None) is not None:
+ answer = resp["solution"].get("text", None)
+ elif resp["solution"].get("gRecaptchaResponse", None) is not None:
+ answer = resp["solution"].get("gRecaptchaResponse", None)
+ return (answer, True)
+ return (resp["taskId"], False)
+ except (requests.RequestException, KeyError):
+ pass
+
+ def __get_answer(self,task_id:int):
+ payload = {"clientKey":self.api_key,"taskId": task_id}
+ for _ in range(100):
+ try:
+ response = requests.post(BASEURL + "/getTaskResult",json=payload,timeout=20,).json()
+ if response["errorId"] != 0: # Means there was an error
+ self.check_error(response["errorDescription"])
+ if response["status"] == "processing":
+ time.sleep(3)
+ continue
+ if self.captcha_type == "normal" or self.captcha_type == "image":
+ return response["solution"]["text"]
+ else:
+ return response["solution"]["gRecaptchaResponse"]
+ except (requests.RequestException, KeyError):
+ pass
+
+ @staticmethod
+ def check_error(error_code):
+ if error_code == "ERROR_ZERO_BALANCE":
+ raise captchaExceptions.NoBalanceException()
+ elif error_code == "ERROR_WRONG_GOOGLEKEY":
+ raise captchaExceptions.WrongSitekeyException()
+ elif error_code == "ERROR_WRONG_USER_KEY" or error_code == "ERROR_KEY_DOES_NOT_EXIST":
+ raise captchaExceptions.WrongAPIKeyException()
+ elif error_code == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
+ raise captchaExceptions.CaptchaIMGTooBig()
+ elif error_code == "ERROR_REQUIRED_FIELDS":
+ raise captchaExceptions.TaskDetails(f"Error: {error_code}")
+ elif error_code == "ERROR_SERVICE_UNAVALIABLE" or error_code == "ERROR_THREADS_MAXIMUM":
+ raise captchaExceptions.NoSlotAvailable("No slot available")
+ elif error_code == "ERROR_IP_NOT_ALLOWED" or error_code == "IP_BANNED" or error_code == "ERROR_IP_BLOCKED":
+ return captchaExceptions.Banned(error_code)
+ elif error_code == "ERROR_ZERO_CAPTCHA_FILESIZE" or error_code == "ERROR_UPLOAD" or \
+ error_code == "ERROR_CAPTCHAIMAGE_BLOCKED" or error_code == "ERROR_IMAGE_TYPE_NOT_SUPPORTED" or \
+ error_code == "ERROR_WRONG_FILE_EXTENSION":
+ raise captchaExceptions.CaptchaImageError(error_code)
+ else: raise captchaExceptions.UnknownError(f"Error returned from Capsovler: {error_code}")
\ No newline at end of file
diff --git a/captchatools/exceptions.py b/captchatools/exceptions.py
index ae91df5..545c329 100644
--- a/captchatools/exceptions.py
+++ b/captchatools/exceptions.py
@@ -1,4 +1,3 @@
-import requests.exceptions
class WrongAPIKeyException(Exception):
'''
This exception gets thrown when the user provides a wrong API Key
@@ -26,8 +25,6 @@ class NoHarvesterException(Exception):
'''
This exception gets thrown when a user doesn't properly set a harvester.
'''
- def __init__(self, message="[captchatools] No captcha harvester selected"):
- super(NoHarvesterException, self).__init__(message)
class CaptchaIMGTooBig(Exception):
'''
@@ -41,4 +38,33 @@ class FailedToGetCapIMG(Exception):
This exception gets thrown when the program fails to get the captcha image after 3 tries
'''
def __init__(self, message="[captchatools] Failed to fetch captcha image."):
- super(FailedToGetCapIMG, self).__init__(message)
\ No newline at end of file
+ super(FailedToGetCapIMG, self).__init__(message)
+
+class Banned(Exception):
+ '''
+ This exception gets thrown when the user is banned from the solving site
+ '''
+
+class TaskDetails(Exception):
+ '''
+ This exceptions gets thrown when there is missing data
+ '''
+
+class NoSlotAvailable(Exception):
+ '''
+ This exceptions gets thrown when there is no worker available
+ '''
+class CaptchaImageError(Exception):
+ '''
+ This exception gets thrown when there is an error with the captcha image
+ '''
+
+class UnknownError(Exception):
+ '''
+ This exceptions gets thrown when there is an unknown error
+ '''
+
+class NoCaptchaType(Exception):
+ '''
+ This exception gets thrown when no captcha type was set
+ '''
\ No newline at end of file
diff --git a/captchatools/harvesters.py b/captchatools/harvesters.py
deleted file mode 100644
index c1af7a7..0000000
--- a/captchatools/harvesters.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from .twocap import Twocap
-from .anticaptcha import Anticap
-from .capmonster import Capmonster
-from .exceptions import NoHarvesterException, FailedToGetCapIMG
-import requests
-import base64
-class captcha_harvesters:
- def __init__( self, solving_site=1,
- api_key="Key the site gave you to solve captchas",
- captcha_type="v2", invisible_captcha=False,
- sitekey="Sitekey of the page where the captcha is loaded",
- captcha_url="The site URL of where the captcha is loaded",
- action="verify", min_score=0.7, soft_id=4782723):
- self.api_key = api_key
- self.solving_site = solving_site
- self.captcha_type = captcha_type.lower()
- self.invisible_captcha = invisible_captcha
- self.captcha_url = captcha_url
- self.min_score = min_score
- self.sitekey = sitekey
- self.action = action
- self.soft_id = soft_id
-
- # Get Token from Capmonster API
- if self.solving_site == 1 or str(self.solving_site).lower() == "capmonster":
- self.harvester = Capmonster(self)
-
- # Get Token from Anticaptcha API
- elif self.solving_site == 2 or str(self.solving_site).lower() == "anticaptcha":
- self.harvester = Anticap(self)
-
- #Get Token from 2captcha API
- elif self.solving_site == 3 or str(self.solving_site).lower() == "2captcha":
- self.harvester = Twocap(self)
- else:
- raise NoHarvesterException(
- "No captcha harvester was selected. Double check you entered the site name correctly "
- +
- "|| Double check the site id is type int"
- )
-
- def get_token(self):
- '''
- Returns a recaptcha token for the provided details
- '''
- return self.harvester.get_token()
-
- def get_normal(self, path_to_image):
- '''
- This method will handle returning text from 'Normal Captchas.'
-
- As per 2captcha,
- "Normal Captcha is an image that contains distored but human-readable text. To solve the captcha user have to type the text from the image."
-
-
- Parameters:
- - path_to_image
- - The path where the captcha is located (you must download the image yourself and then pass it in)
- '''
- return self.harvester.get_normal(path_to_image)
-
- @staticmethod
- def get_cap_img(img_url:str) -> str:
- '''
- This function tries 3 times to get the captcha image.
-
- If successful, it returns the base64 encoded image.
- If not successful, raises a FailedToGetCapIMG exception
- '''
- for _ in range(3):
- try:
- response = requests.get(img_url)
- b64response = base64.b64encode(response.content).decode("utf-8")
- return b64response
- except:
- pass
- raise FailedToGetCapIMG()
\ No newline at end of file
diff --git a/captchatools/twocap.py b/captchatools/twocap.py
index e0fbc41..2df89f7 100644
--- a/captchatools/twocap.py
+++ b/captchatools/twocap.py
@@ -1,93 +1,117 @@
+from . import Harvester
import requests
from . import exceptions as captchaExceptions
+from typing import Optional
import time
-BASEURL = "http://2captcha.com/in.php"
+BASEURL_IN = "http://2captcha.com/in.php"
-# There's others way we could've done this,
-# but it's sufficient for right now.
-class Twocap:
- '''
- This object will contain the data to interact with 2captcha.com API
- '''
- def __init__(self, parent:object):
- self.user_data = parent
-
- def get_token(self) -> str:
- return self.get_answer( self.get_id() )
+class Twocap(Harvester):
+ def get_balance(self) -> float:
+ url = f"https://2captcha.com/res.php?key={self.api_key}&action=getbalance&json=1"
+ for _ in range(5):
+ try:
+ resp = requests.get(url, timeout=20).json()
+ if resp["status"] == 0: # Means there was an error
+ self.check_error(resp["request"])
+ return float(resp["request"])
+ except requests.RequestException:
+ pass
- def get_normal(self, cap_pic_url) -> str:
- return self.get_answer( self.get_id(cap_pic_url) )
+ def get_token(self, b64_img: Optional[str] = None, user_agent: Optional[str] = None, proxy: Optional[str] = None, proxy_type: Optional[str] = None):
+ # Get ID
+ task_id = self.__get_id(
+ b64_img=b64_img,
+ user_agent=user_agent,
+ proxy=proxy,
+ proxy_type=proxy_type
+ )
+
+ # Get Answer
+ return self.__get_answer(task_id)
+
+ def __create_payload(self, **kwargs):
+ payload = {"key": self.api_key,"json": 1}
- def get_id(self, cap_pic_url=None) -> int:
- payload = {
- "key": self.user_data.api_key,
- "method": "userrecaptcha", # Because V2 recaptcha is defaulted on init, I'll leave this
- "googlekey": self.user_data.sitekey,
- "pageurl":self.user_data.captcha_url,
- "json": 1
- }
- if self.user_data.soft_id is not None:
- payload["soft_id"] = self.user_data.soft_id
- if self.user_data.captcha_type == "v2":
- if self.user_data.invisible_captcha:
+ # Add data based on the captcha type
+ if self.captcha_type == "image" or self.captcha_type == "normal":
+ payload["method"] = "base64"
+ payload["body"] = kwargs.get("b64_img", "")
+ elif self.captcha_type == "v2":
+ payload["method"] = "userrecaptcha"
+ payload["googlekey"] = self.sitekey
+ payload["pageurl"] = self.captcha_url
+ if self.invisible_captcha:
payload["invisible"] = 1
-
- elif self.user_data.captcha_type == "v3":
+ elif self.captcha_type == "v3":
+ payload["method"] = "userrecaptcha"
payload["version"] = "v3"
- payload["action"] = self.user_data.action
- payload["min_score"] = self.user_data.min_score
-
- elif self.user_data.captcha_type == "hcap" or self.user_data.captcha_type == "hcaptcha":
+ payload["action"] = self.action
+ payload["googlekey"] = self.sitekey
+ payload["pageurl"] = self.captcha_url
+ if self.min_score is not None:
+ payload["min_score"] = self.min_score
+ elif self.captcha_type == "hcap" or self.captcha_type == "hcaptcha":
payload["method"] = "hcaptcha"
- # We need to remove the "googlekey" ket from the payload
- # And replace it with "sitekey"
- payload.pop("googlekey")
- payload["sitekey"] = self.user_data.sitekey
+ payload["sitekey"] = self.sitekey
+ payload["pageurl"] = self.captcha_url
- elif self.user_data.captcha_type == "normal":
- payload["method"] = "base64"
- payload["body"] = self.user_data.get_cap_img(cap_pic_url)
- while True:
+ # Add Global Data
+ if self.soft_id is not None:
+ payload["soft_id"] = self.soft_id
+ if kwargs.get("proxy", None) is not None:
+ payload["proxy"] = kwargs.get("proxy")
+ pxy_type = kwargs.get("proxy_type", "http")
+ payload["proxytype"] = pxy_type
+ if kwargs.get("user_agent", None) is not None:
+ payload["userAgent"] = kwargs.get("user_agent")
+ return payload
+
+ def __get_id(self,**kwargs):
+ # Create Payload
+ payload = self.__create_payload(**kwargs)
+
+ # Get token & return it
+ for _ in range(50):
try:
- resp = requests.post(BASEURL, data=payload).json()
- if resp["status"] == 1:
- return resp["request"] # Return the queue ID
-
- elif resp["request"] == "ERROR_ZERO_BALANCE":
- # Throw Exception
- raise captchaExceptions.NoBalanceException()
-
- elif resp["request"] == "ERROR_WRONG_GOOGLEKEY":
- # Throw Exception
- raise captchaExceptions.WrongSitekeyException()
-
- elif resp["request"] == "ERROR_WRONG_USER_KEY" or resp["request"] == "ERROR_KEY_DOES_NOT_EXIST":
- # Throw Exception
- raise captchaExceptions.WrongAPIKeyException()
-
- elif resp["request"] == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
- raise captchaExceptions.CaptchaIMGTooBig()
- break
-
- except requests.RequestException:
+ resp = requests.post(BASEURL_IN, json=payload, timeout=20).json()
+ if resp["status"] == 0: # Means there was an error:
+ self.check_error(resp["request"])
+ return resp["request"]
+ except (requests.RequestException, KeyError):
pass
-
- def get_answer(self, queue_id) -> str:
- '''
- This method gets the captcha token from the API
- '''
- while True:
+
+ def __get_answer(self,task_id:int):
+ for _ in range(100):
try:
-
- answer = requests.get(f"http://2captcha.com/res.php?key={self.user_data.api_key}&action=get&id={queue_id}&json=1",timeout=10,).json()
- if answer["status"] == 1: # Solved
- return answer["request"]
- elif answer["request"] == "ERROR_CAPTCHA_UNSOLVABLE":
- self.get_token()
- time.sleep(4)
-
- except KeyError:
- self.get_token()
- except Exception:
- pass
\ No newline at end of file
+ response = requests.get(f"http://2captcha.com/res.php?key={self.api_key}&action=get&id={task_id}&json=1",timeout=20,).json()
+ if response["status"] == 0 and response["request"] != "CAPCHA_NOT_READY": # Error checking
+ self.check_error(response["request"])
+ if response["status"] == 0 and response["request"] == "CAPCHA_NOT_READY":
+ time.sleep(4)
+ continue
+ return response["request"] # Return the captcha token
+ except (requests.RequestException, KeyError):
+ pass
+
+ @staticmethod
+ def check_error(error_code):
+ if error_code == "ERROR_ZERO_BALANCE":
+ raise captchaExceptions.NoBalanceException()
+ elif error_code == "ERROR_WRONG_GOOGLEKEY":
+ raise captchaExceptions.WrongSitekeyException()
+ elif error_code == "ERROR_WRONG_USER_KEY" or error_code == "ERROR_KEY_DOES_NOT_EXIST":
+ raise captchaExceptions.WrongAPIKeyException()
+ elif error_code == "ERROR_TOO_BIG_CAPTCHA_FILESIZE":
+ raise captchaExceptions.CaptchaIMGTooBig()
+ elif error_code == "ERROR_PAGEURL":
+ raise captchaExceptions.TaskDetails(f"Error: {error_code}")
+ elif error_code == "MAX_USER_TURN" or error_code == "ERROR_NO_SLOT_AVAILABLE":
+ raise captchaExceptions.NoSlotAvailable("No slot available")
+ elif error_code == "ERROR_IP_NOT_ALLOWED" or error_code == "IP_BANNED":
+ return captchaExceptions.Banned(error_code)
+ elif error_code == "ERROR_ZERO_CAPTCHA_FILESIZE" or error_code == "ERROR_UPLOAD" or \
+ error_code == "ERROR_CAPTCHAIMAGE_BLOCKED" or error_code == "ERROR_IMAGE_TYPE_NOT_SUPPORTED" or \
+ error_code == "ERROR_WRONG_FILE_EXTENSION":
+ raise captchaExceptions.CaptchaImageError(error_code)
+ else: raise captchaExceptions.UnknownError(f"Error returned from 2captcha: {error_code}")
\ No newline at end of file
diff --git a/setup.py b/setup.py
index f582beb..29785d0 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
from io import open
PACKAGE_NAME = "captchatools"
-VERSION = "1.2.1"
+VERSION = "1.3.0"
SHORT_DESCRIPTION = "Python module to help solve captchas with Capmonster, 2captcha and Anticaptcha API's!"
GITHUB_URL = "https://github.com/Matthew17-21/Captcha-Tools"
diff --git a/tests/harvester.py b/tests/harvester.py
new file mode 100644
index 0000000..0cf4536
--- /dev/null
+++ b/tests/harvester.py
@@ -0,0 +1,35 @@
+import unittest
+import sys
+import os
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+sys.path.append(os.path.dirname(SCRIPT_DIR))
+import captchatools
+from captchatools import new_harvester, exceptions as captchaExceptions
+from unittest import mock
+
+def mock_get_response(*args, **kwargs):
+ class MockResponse:
+ def __init__(self, json_data):
+ self.json_data = json_data
+ def json(self):
+ return self.json_data
+
+ if args[0] == "https://2captcha.com/res.php?key=real_key&action=getbalance&json=1" :
+ return MockResponse({"status":1,"request":"17.87942"})
+ if args[0] == "https://2captcha.com/res.php?key=fake_key&action=getbalance&json=1" :
+ return MockResponse({"status":0,"request":"ERROR_WRONG_USER_KEY","error_text":"You've provided key parameter value in incorrect format], it should contain 32 symbols."})
+
+
+class Test2Captcha(unittest.TestCase):
+ '''
+ These tests ensure everything is working correcty with the 2captcha class
+ '''
+ @mock.patch('requests.get', side_effect=mock_get_response)
+ def test_get_balance(self, mock_get):
+ real = new_harvester(api_key="real_key", solving_site="2captcha")
+ self.assertEqual(real.get_balance(), 17.87942)
+ fake = new_harvester(api_key="fake_key", solving_site="2captcha")
+ self.assertRaises(captchaExceptions.WrongAPIKeyException, fake.get_balance)
+
+if __name__ == "__main__":
+ unittest.main()
\ No newline at end of file