From b13e7c58e842a7e9e8b22746fd8cc164c8c31c24 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 3 Dec 2022 18:09:41 -0500 Subject: [PATCH 01/24] Made Harvester an abstract class --- captchatools/harvester.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 captchatools/harvester.py diff --git a/captchatools/harvester.py b/captchatools/harvester.py new file mode 100644 index 0000000..2151092 --- /dev/null +++ b/captchatools/harvester.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod + +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 Exception("No API Key!") + if self.solving_site is None: + raise Exception("No solving site set") + if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap"]: + raise Exception("Invalid captcha type") + if self.soft_id is None: # Set with my own soft_id + pass + + @abstractmethod + def get_balance(self) -> float: + ''' + Returns the balance for the current captcha harvster + ''' + + @abstractmethod + def get_token(self): + ''' + Returns a captcha token + ''' \ No newline at end of file From 226acae9069dc0f7c8484db8311659ec01ad207c Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 3 Dec 2022 18:24:29 -0500 Subject: [PATCH 02/24] Added gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file From 0dba62f7cf94716f55e1f6639d9a857433078da2 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 3 Dec 2022 18:43:07 -0500 Subject: [PATCH 03/24] Added harvester tests --- tests/harvester.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/harvester.py diff --git a/tests/harvester.py b/tests/harvester.py new file mode 100644 index 0000000..1a98051 --- /dev/null +++ b/tests/harvester.py @@ -0,0 +1,19 @@ +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 + + +class TestHarvester(unittest.TestCase): + ''' + This test ensures that everything initializes correctly + ''' + def test_2captcha(self): + h = new_harvester(api_key="key_here", solving_site="2captcha") + self.assertTrue(isinstance(h, captchatools.Twocap), "Not type captchatools.Twocaptcha") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From b937907b7558d9e7ceda527f7e7b13f2fbe83bcb Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 3 Dec 2022 18:45:32 -0500 Subject: [PATCH 04/24] Update __init__.py --- captchatools/__init__.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index e4ae0e2..dc58e6f 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -16,7 +16,27 @@ 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 .harvester import Harvester +from .twocap import Twocap +from .exceptions import ( + NoHarvesterException +) + +def new_harvester(**kwargs) -> Harvester: + ''' + Returns a new captcha harvester with the specified config + ''' + site = kwargs.get("solving_site",None) + if site is None: + raise NoHarvesterException( + "No captcha harvester was selected. Double check you entered the site name correctly " + + + "|| Double check the site id is type int" + ) + + if site == 3 or str(site).lower() == "2captcha": + return Twocap(**kwargs) From ae938dd8ba762a84a4035ad87da3bbba50f3e8c5 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 3 Dec 2022 19:27:44 -0500 Subject: [PATCH 05/24] Added stub file for init file --- captchatools/__init__.pyi | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 captchatools/__init__.pyi diff --git a/captchatools/__init__.pyi b/captchatools/__init__.pyi new file mode 100644 index 0000000..a1cd44f --- /dev/null +++ b/captchatools/__init__.pyi @@ -0,0 +1,15 @@ +# https://mypy.readthedocs.io/en/stable/stubs.html +from .harvester import Harvester + + +def new_harvester( + api_key : str, + captcha_type : str, + solving_site : str | int, + invisible_captcha : bool, + captcha_url : int, + min_score : float, + sitekey : str, + action : str, + soft_id : str | int + ) -> Harvester: ... \ No newline at end of file From d4b393428099c91f5267cca5bba23508409efbd1 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 3 Dec 2022 21:05:03 -0500 Subject: [PATCH 06/24] Get 2captcha balance --- captchatools/twocap.py | 162 +++++++++++++++++++++++------------------ tests/harvester.py | 25 ++++++- 2 files changed, 117 insertions(+), 70 deletions(-) diff --git a/captchatools/twocap.py b/captchatools/twocap.py index e0fbc41..088ea94 100644 --- a/captchatools/twocap.py +++ b/captchatools/twocap.py @@ -1,93 +1,117 @@ import requests from . import exceptions as captchaExceptions +from . import Harvester import time BASEURL = "http://2captcha.com/in.php" # There's others way we could've done this, # but it's sufficient for right now. -class Twocap: +class Twocap(Harvester): ''' This object will contain the data to interact with 2captcha.com API - ''' - def __init__(self, parent:object): - self.user_data = parent + ''' + 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).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_token(self): + return "token from 2captcha" + + @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() + else: raise Exception(f"Error returned from 2captcha: {error_code}") - def get_token(self) -> str: - return self.get_answer( self.get_id() ) + # def get_token(self) -> str: + # return self.get_answer( self.get_id() ) - def get_normal(self, cap_pic_url) -> str: - return self.get_answer( self.get_id(cap_pic_url) ) + # def get_normal(self, cap_pic_url) -> str: + # return self.get_answer( self.get_id(cap_pic_url) ) - 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: - payload["invisible"] = 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: + # payload["invisible"] = 1 - elif self.user_data.captcha_type == "v3": - payload["version"] = "v3" - payload["action"] = self.user_data.action - payload["min_score"] = self.user_data.min_score + # elif self.user_data.captcha_type == "v3": + # 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["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 + # elif self.user_data.captcha_type == "hcap" or self.user_data.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 - elif self.user_data.captcha_type == "normal": - payload["method"] = "base64" - payload["body"] = self.user_data.get_cap_img(cap_pic_url) - while True: - try: - resp = requests.post(BASEURL, data=payload).json() - if resp["status"] == 1: - return resp["request"] # Return the queue ID + # elif self.user_data.captcha_type == "normal": + # payload["method"] = "base64" + # payload["body"] = self.user_data.get_cap_img(cap_pic_url) + # while True: + # 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_ZERO_BALANCE": + # # Throw Exception + # raise captchaExceptions.NoBalanceException() - elif resp["request"] == "ERROR_WRONG_GOOGLEKEY": - # Throw Exception - raise captchaExceptions.WrongSitekeyException() + # 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_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 + # elif resp["request"] == "ERROR_TOO_BIG_CAPTCHA_FILESIZE": + # raise captchaExceptions.CaptchaIMGTooBig() + # break - except requests.RequestException: - pass + # except requests.RequestException: + # pass - def get_answer(self, queue_id) -> str: - ''' - This method gets the captcha token from the API - ''' - while True: - try: + # def get_answer(self, queue_id) -> str: + # ''' + # This method gets the captcha token from the API + # ''' + # while True: + # 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) + # 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 + # except KeyError: + # self.get_token() + # except Exception: + # pass \ No newline at end of file diff --git a/tests/harvester.py b/tests/harvester.py index 1a98051..8ed7103 100644 --- a/tests/harvester.py +++ b/tests/harvester.py @@ -4,8 +4,20 @@ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.dirname(SCRIPT_DIR)) import captchatools -from captchatools import new_harvester +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 TestHarvester(unittest.TestCase): ''' @@ -15,5 +27,16 @@ def test_2captcha(self): h = new_harvester(api_key="key_here", solving_site="2captcha") self.assertTrue(isinstance(h, captchatools.Twocap), "Not type captchatools.Twocaptcha") +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") + fake = new_harvester(api_key="fake_key", solving_site="2captcha") + self.assertEqual(real.get_balance(), 17.87942) + self.assertRaises(captchaExceptions.WrongAPIKeyException, fake.get_balance) + if __name__ == "__main__": unittest.main() \ No newline at end of file From 855523c864f50a28418094c7dde88bf139ba1c19 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 4 Dec 2022 17:50:51 -0500 Subject: [PATCH 07/24] Moved Harvester to appropriate file --- captchatools/__init__.py | 55 ++++++++++++++++------- captchatools/__init__.pyi | 15 ------- captchatools/harvester.py | 38 ---------------- captchatools/twocap.py | 95 ++------------------------------------- 4 files changed, 42 insertions(+), 161 deletions(-) delete mode 100644 captchatools/__init__.pyi delete mode 100644 captchatools/harvester.py diff --git a/captchatools/__init__.py b/captchatools/__init__.py index dc58e6f..64e437f 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -20,23 +20,44 @@ __author__ = "Matthew17-21" __license__ = "MIT" -from .harvester import Harvester -from .twocap import Twocap -from .exceptions import ( - NoHarvesterException -) - -def new_harvester(**kwargs) -> Harvester: +from abc import ABC, abstractmethod +class Harvester(ABC): ''' - Returns a new captcha harvester with the specified config + Represents a captcha harvester. ''' - site = kwargs.get("solving_site",None) - if site is None: - raise NoHarvesterException( - "No captcha harvester was selected. Double check you entered the site name correctly " - + - "|| Double check the site id is type int" - ) + 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) - if site == 3 or str(site).lower() == "2captcha": - return Twocap(**kwargs) + # Validate Data + if self.api_key is None: + raise Exception("No API Key!") + if self.solving_site is None: + raise Exception("No solving site set") + if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap"]: + raise Exception("Invalid captcha type") + if self.soft_id is None: # Set with my own soft_id + pass + + @abstractmethod + def get_balance(self) -> float: + ''' + Returns the balance for the current captcha harvster + ''' + + @abstractmethod + def get_token(self): + ''' + Returns a captcha token + ''' + +def new_harvester(**kwargs) -> Harvester: + from .twocap import Twocap + return Twocap(**kwargs) \ No newline at end of file diff --git a/captchatools/__init__.pyi b/captchatools/__init__.pyi deleted file mode 100644 index a1cd44f..0000000 --- a/captchatools/__init__.pyi +++ /dev/null @@ -1,15 +0,0 @@ -# https://mypy.readthedocs.io/en/stable/stubs.html -from .harvester import Harvester - - -def new_harvester( - api_key : str, - captcha_type : str, - solving_site : str | int, - invisible_captcha : bool, - captcha_url : int, - min_score : float, - sitekey : str, - action : str, - soft_id : str | int - ) -> Harvester: ... \ No newline at end of file diff --git a/captchatools/harvester.py b/captchatools/harvester.py deleted file mode 100644 index 2151092..0000000 --- a/captchatools/harvester.py +++ /dev/null @@ -1,38 +0,0 @@ -from abc import ABC, abstractmethod - -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 Exception("No API Key!") - if self.solving_site is None: - raise Exception("No solving site set") - if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap"]: - raise Exception("Invalid captcha type") - if self.soft_id is None: # Set with my own soft_id - pass - - @abstractmethod - def get_balance(self) -> float: - ''' - Returns the balance for the current captcha harvster - ''' - - @abstractmethod - def get_token(self): - ''' - Returns a captcha token - ''' \ No newline at end of file diff --git a/captchatools/twocap.py b/captchatools/twocap.py index 088ea94..f850470 100644 --- a/captchatools/twocap.py +++ b/captchatools/twocap.py @@ -1,16 +1,8 @@ +from . import Harvester import requests from . import exceptions as captchaExceptions -from . import Harvester -import time -BASEURL = "http://2captcha.com/in.php" - -# There's others way we could've done this, -# but it's sufficient for right now. class Twocap(Harvester): - ''' - This object will contain the data to interact with 2captcha.com API - ''' def get_balance(self) -> float: url = f"https://2captcha.com/res.php?key={self.api_key}&action=getbalance&json=1" for _ in range(5): @@ -21,9 +13,9 @@ def get_balance(self) -> float: return float(resp["request"]) except requests.RequestException: pass - + def get_token(self): - return "token from 2captcha" + return "2captcha_token" @staticmethod def check_error(error_code): @@ -35,83 +27,4 @@ def check_error(error_code): raise captchaExceptions.WrongAPIKeyException() elif error_code == "ERROR_TOO_BIG_CAPTCHA_FILESIZE": raise captchaExceptions.CaptchaIMGTooBig() - else: raise Exception(f"Error returned from 2captcha: {error_code}") - - # def get_token(self) -> str: - # return self.get_answer( self.get_id() ) - - # def get_normal(self, cap_pic_url) -> str: - # return self.get_answer( self.get_id(cap_pic_url) ) - - # 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: - # payload["invisible"] = 1 - - # elif self.user_data.captcha_type == "v3": - # 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["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 - - # elif self.user_data.captcha_type == "normal": - # payload["method"] = "base64" - # payload["body"] = self.user_data.get_cap_img(cap_pic_url) - # while True: - # 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: - # pass - - # def get_answer(self, queue_id) -> str: - # ''' - # This method gets the captcha token from the API - # ''' - # while True: - # 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 + else: raise Exception(f"Error returned from 2captcha: {error_code}") \ No newline at end of file From d0ae5b5af926d205f90161bcf7915f8bb1021d0f Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 11:50:43 -0500 Subject: [PATCH 08/24] Added additional data --- captchatools/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index 64e437f..f7bb211 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -21,6 +21,7 @@ __license__ = "MIT" from abc import ABC, abstractmethod +from typing import Optional class Harvester(ABC): ''' Represents a captcha harvester. @@ -53,7 +54,12 @@ def get_balance(self) -> float: ''' @abstractmethod - def get_token(self): + 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 ''' From 17c18b0d90de86132034896fa442a1a53b61d070 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 14:15:34 -0500 Subject: [PATCH 09/24] Updated 2captcha --- captchatools/twocap.py | 93 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/captchatools/twocap.py b/captchatools/twocap.py index f850470..f6b5f7c 100644 --- a/captchatools/twocap.py +++ b/captchatools/twocap.py @@ -1,21 +1,98 @@ from . import Harvester import requests from . import exceptions as captchaExceptions +from typing import Optional +import time + +BASEURL_IN = "http://2captcha.com/in.php" 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).json() + 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_token(self): - return "2captcha_token" + 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} + + # 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.captcha_type == "v3": + payload["method"] = "userrecaptcha" + payload["version"] = "v3" + 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" + payload["sitekey"] = self.sitekey + payload["pageurl"] = self.captcha_url + + # 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_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,task_id:int): + for _ in range(100): + try: + 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): @@ -27,4 +104,14 @@ def check_error(error_code): 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 Exception(f"Error returned from 2captcha: {error_code}") \ No newline at end of file From 9d1fb703ebb3a3fa3d5545fcf5fd3b49c70721c3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 14:15:46 -0500 Subject: [PATCH 10/24] Added captcha image --- captchatools/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index f7bb211..c981e77 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -42,9 +42,9 @@ def __init__(self, **kwargs) -> None: raise Exception("No API Key!") if self.solving_site is None: raise Exception("No solving site set") - if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap"]: + if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap", "image", "normal"]: raise Exception("Invalid captcha type") - if self.soft_id is None: # Set with my own soft_id + if self.soft_id is None: #TODO Set with my own soft_id pass @abstractmethod From 09a87f72770ffc82bc43d6f4cac454c23362ea0b Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 14:15:55 -0500 Subject: [PATCH 11/24] Added more exceptions --- captchatools/exceptions.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/captchatools/exceptions.py b/captchatools/exceptions.py index ae91df5..c8c3586 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 @@ -41,4 +40,23 @@ 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 + ''' \ No newline at end of file From b62a66b4d7a89e67f767667870b3d15627907852 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 14:18:04 -0500 Subject: [PATCH 12/24] Update 22captcha test --- tests/harvester.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/harvester.py b/tests/harvester.py index 8ed7103..0cf4536 100644 --- a/tests/harvester.py +++ b/tests/harvester.py @@ -19,13 +19,6 @@ def json(self): 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 TestHarvester(unittest.TestCase): - ''' - This test ensures that everything initializes correctly - ''' - def test_2captcha(self): - h = new_harvester(api_key="key_here", solving_site="2captcha") - self.assertTrue(isinstance(h, captchatools.Twocap), "Not type captchatools.Twocaptcha") class Test2Captcha(unittest.TestCase): ''' @@ -34,8 +27,8 @@ class Test2Captcha(unittest.TestCase): @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") - fake = new_harvester(api_key="fake_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__": From ab7b83886664131eb32c53ca49739ef4cc5efb0d Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 14:27:18 -0500 Subject: [PATCH 13/24] backward compatibility --- captchatools/__init__.py | 6 ++- captchatools/harvesters.py | 77 -------------------------------------- 2 files changed, 5 insertions(+), 78 deletions(-) delete mode 100644 captchatools/harvesters.py diff --git a/captchatools/__init__.py b/captchatools/__init__.py index c981e77..c2fa87c 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -66,4 +66,8 @@ def get_token( def new_harvester(**kwargs) -> Harvester: from .twocap import Twocap - return Twocap(**kwargs) \ No newline at end of file + return Twocap(**kwargs) + +# Just for backward compatibility +def captcha_harvesters(**kwargs) -> Harvester: + return new_harvester(**kwargs) \ 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 From 5c6c5ca9e6700fe32bb0b0eeb44b5d5944ad3ce1 Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 9 Dec 2022 16:43:56 -0500 Subject: [PATCH 14/24] Updated Anticaptcha --- captchatools/__init__.py | 11 +- captchatools/anticaptcha.py | 205 ++++++++++++++++++++---------------- 2 files changed, 123 insertions(+), 93 deletions(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index c2fa87c..bc9e1ce 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -65,8 +65,17 @@ def get_token( ''' def new_harvester(**kwargs) -> Harvester: + # Need to import here to prevent circular imports from .twocap import Twocap - return Twocap(**kwargs) + from .anticaptcha import Anticaptcha + + site = kwargs.get("solving_site","").lower() + if site == 2 or site == "anticaptcha": + return Anticaptcha(**kwargs) + elif site == 3 or site == "2captcha": + return Twocap(**kwargs) + + # Just for backward compatibility def captcha_harvesters(**kwargs) -> Harvester: diff --git a/captchatools/anticaptcha.py b/captchatools/anticaptcha.py index bfd4b60..06023ac 100644 --- a/captchatools/anticaptcha.py +++ b/captchatools/anticaptcha.py @@ -1,106 +1,127 @@ -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] = 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 get_normal(self, cap_pic_url) -> str: - return self.get_answer(self.get_id(cap_pic_url), 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": - 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) + 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: - 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: + # 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] + 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, queue_id, isTextCap=False) -> str: - ''' - This method gets the captcha token from the API - ''' - - # Get the captcha token from their API - while True: + + def __get_answer(self,task_id:int): + payload = {"clientKey":self.api_key,"taskId": task_id} + for _ in range(100): 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() - 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["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": + 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 Exception(f"Error returned from 2captcha: {error_code}") From f325b810fa59b0705fa11dda57c108d3862a6505 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 11:07:34 -0500 Subject: [PATCH 15/24] Added more errors for anticaptcha --- captchatools/anticaptcha.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/captchatools/anticaptcha.py b/captchatools/anticaptcha.py index 06023ac..812763f 100644 --- a/captchatools/anticaptcha.py +++ b/captchatools/anticaptcha.py @@ -118,7 +118,7 @@ def check_error(error_code): 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": + 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 \ From 19e3cda25d3c2268d7d26b92a19d637faefbf9a7 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 11:27:10 -0500 Subject: [PATCH 16/24] Updated capmonster --- captchatools/__init__.py | 7 +- captchatools/capmonster.py | 197 +++++++++++++++++++++---------------- 2 files changed, 118 insertions(+), 86 deletions(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index bc9e1ce..b2fce72 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -68,13 +68,16 @@ 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 site = kwargs.get("solving_site","").lower() - if site == 2 or site == "anticaptcha": + 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) - + #TODO should throw an exception here # Just for backward compatibility diff --git a/captchatools/capmonster.py b/captchatools/capmonster.py index 8ef4058..16ba251 100644 --- a/captchatools/capmonster.py +++ b/captchatools/capmonster.py @@ -1,104 +1,133 @@ -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] = 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 get_normal(self, path_to_img) -> str: - return self.get_answer(self.get_id(path_to_img), True) - - 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 - } - } + def __create_payload(self, **kwargs): + payload = {"clientKey":self.api_key,"task":{}} - # 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] + 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).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: + 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["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 Exception(f"Error returned from 2captcha: {error_code}") \ No newline at end of file From 5e4754ef82377c3cdf43f08f06535c51ff666b02 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 21:40:31 -0500 Subject: [PATCH 17/24] Update anticaptcha.py --- captchatools/anticaptcha.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/captchatools/anticaptcha.py b/captchatools/anticaptcha.py index 812763f..3824ee3 100644 --- a/captchatools/anticaptcha.py +++ b/captchatools/anticaptcha.py @@ -18,7 +18,7 @@ def get_balance(self) -> float: 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] = None): + 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, @@ -64,8 +64,12 @@ def __create_payload(self, **kwargs): 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] - payload["task"]["proxyPort"] = splitted[1] + 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] From 96a84752952e4a9b829ca6d3fc745ac1c4ca3fa3 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 22:19:08 -0500 Subject: [PATCH 18/24] Update capmonster.py --- captchatools/capmonster.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/captchatools/capmonster.py b/captchatools/capmonster.py index 16ba251..c5ae711 100644 --- a/captchatools/capmonster.py +++ b/captchatools/capmonster.py @@ -23,7 +23,7 @@ def get_balance(self) -> float: 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] = None): + 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, @@ -70,7 +70,10 @@ def __create_payload(self, **kwargs): if kwargs.get("proxy", None) is not None: splitted = kwargs.get("proxy").split(":") payload["task"]["proxyAddress"] = splitted[0] - payload["task"]["proxyPort"] = splitted[1] + 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] @@ -86,7 +89,7 @@ def __get_id(self,**kwargs): # Get token & return it for _ in range(50): try: - resp = requests.post(BASEURL + "/createTask " , json=payload, timeout=20).json() + 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"] @@ -99,7 +102,7 @@ def __get_answer(self,task_id:int): 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"]) + self.check_error(response["errorCode"]) if response["status"] == "processing": time.sleep(3) continue @@ -130,4 +133,4 @@ def check_error(error_code): 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 Exception(f"Error returned from 2captcha: {error_code}") \ No newline at end of file + else: raise Exception(f"Error returned from anticaptcha: {error_code}") \ No newline at end of file From a9fb29bab532eb61523c5f2dd6e93f6ef0f24b54 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 22:20:05 -0500 Subject: [PATCH 19/24] Added new exception --- captchatools/anticaptcha.py | 2 +- captchatools/capmonster.py | 2 +- captchatools/exceptions.py | 5 +++++ captchatools/twocap.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/captchatools/anticaptcha.py b/captchatools/anticaptcha.py index 3824ee3..cdfa1c5 100644 --- a/captchatools/anticaptcha.py +++ b/captchatools/anticaptcha.py @@ -128,4 +128,4 @@ def check_error(error_code): 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 Exception(f"Error returned from 2captcha: {error_code}") + else: raise captchaExceptions.UnknownError(f"Error returned from 2captcha: {error_code}") diff --git a/captchatools/capmonster.py b/captchatools/capmonster.py index c5ae711..eed930f 100644 --- a/captchatools/capmonster.py +++ b/captchatools/capmonster.py @@ -133,4 +133,4 @@ def check_error(error_code): 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 Exception(f"Error returned from anticaptcha: {error_code}") \ No newline at end of file + else: raise captchaExceptions.UnknownError(f"Error returned from anticaptcha: {error_code}") \ No newline at end of file diff --git a/captchatools/exceptions.py b/captchatools/exceptions.py index c8c3586..d73234c 100644 --- a/captchatools/exceptions.py +++ b/captchatools/exceptions.py @@ -59,4 +59,9 @@ class NoSlotAvailable(Exception): 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 ''' \ No newline at end of file diff --git a/captchatools/twocap.py b/captchatools/twocap.py index f6b5f7c..2df89f7 100644 --- a/captchatools/twocap.py +++ b/captchatools/twocap.py @@ -114,4 +114,4 @@ def check_error(error_code): 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 Exception(f"Error returned from 2captcha: {error_code}") \ No newline at end of file + else: raise captchaExceptions.UnknownError(f"Error returned from 2captcha: {error_code}") \ No newline at end of file From 314f0d0533240de819bc9e779e3ae592858a6e09 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 22:26:34 -0500 Subject: [PATCH 20/24] Unimportant change --- captchatools/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index b2fce72..1a8ad02 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -44,8 +44,9 @@ def __init__(self, **kwargs) -> None: raise Exception("No solving site set") if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap", "image", "normal"]: raise Exception("Invalid captcha type") - if self.soft_id is None: #TODO Set with my own soft_id - pass + 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: From 84325ae480c3a564cbca0756501bf9f0ae09a433 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sat, 10 Dec 2022 22:31:44 -0500 Subject: [PATCH 21/24] Better exceptions --- captchatools/__init__.py | 9 +++++---- captchatools/exceptions.py | 7 +++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/captchatools/__init__.py b/captchatools/__init__.py index 1a8ad02..6cd912d 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -22,6 +22,7 @@ from abc import ABC, abstractmethod from typing import Optional +from . import exceptions as captchaExceptions class Harvester(ABC): ''' Represents a captcha harvester. @@ -39,11 +40,11 @@ def __init__(self, **kwargs) -> None: # Validate Data if self.api_key is None: - raise Exception("No API Key!") + raise captchaExceptions.WrongAPIKeyException() if self.solving_site is None: - raise Exception("No solving site set") + raise captchaExceptions.NoHarvesterException("No solving site selected") if self.captcha_type not in ["v2", "v3", "hcaptcha", "hcap", "image", "normal"]: - raise Exception("Invalid captcha type") + 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 @@ -78,7 +79,7 @@ def new_harvester(**kwargs) -> Harvester: return Anticaptcha(**kwargs) elif site == 3 or site == "2captcha": return Twocap(**kwargs) - #TODO should throw an exception here + raise captchaExceptions.NoHarvesterException("No solving site selected") # Just for backward compatibility diff --git a/captchatools/exceptions.py b/captchatools/exceptions.py index d73234c..545c329 100644 --- a/captchatools/exceptions.py +++ b/captchatools/exceptions.py @@ -25,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): ''' @@ -64,4 +62,9 @@ class CaptchaImageError(Exception): 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 From d29850efdfc979806b8febc1009a600fc18046c0 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 11 Dec 2022 16:48:25 -0500 Subject: [PATCH 22/24] Added capsolver --- captchatools/__init__.py | 3 + captchatools/capsolver.py | 156 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 captchatools/capsolver.py diff --git a/captchatools/__init__.py b/captchatools/__init__.py index 6cd912d..c24f399 100644 --- a/captchatools/__init__.py +++ b/captchatools/__init__.py @@ -71,6 +71,7 @@ def new_harvester(**kwargs) -> Harvester: 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": @@ -79,6 +80,8 @@ def new_harvester(**kwargs) -> Harvester: 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") 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 From bc85c2034448eceb6dde28c0ffdd61e1524be91f Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 11 Dec 2022 17:02:52 -0500 Subject: [PATCH 23/24] Added type hints for harvester params --- captchatools/__init__.pyi | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 captchatools/__init__.pyi 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 From 3da04a995b1a6f0e367723a719cf7e5df2e21871 Mon Sep 17 00:00:00 2001 From: Matthew Date: Sun, 11 Dec 2022 18:01:04 -0500 Subject: [PATCH 24/24] Update Readme and stuff needed for PyPl --- .gitignore | 5 +- README.md | 186 +++++++++++++++++++++++++++++++++-------------------- setup.py | 2 +- 3 files changed, 123 insertions(+), 70 deletions(-) diff --git a/.gitignore b/.gitignore index ba0430d..660cf96 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -__pycache__/ \ No newline at end of file +__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="iVBORw0KGgoAAAANSUhEUgAAAUQAAAAxCAYAAACictAAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGt0lEQVR4nO1di23bMBC9bKBsIG+QbqBOUHcCa4N4A2uDeIIqGzgTRCNkg2qDaIO6EEABgmDxezz+7gEEglbm8emOj38KgMFgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMhjleAKAFgA4ArgDQi79fAaABgCohDq8rDv2KSyueyQE5+IuSV70TG1eR/1E8w7DEOwDcd9KYiK1GBMQkyX+dBgA4IVY2V15L+QcDDkvevUEFGAzyNklzBQ3pr1x5gfi/WfxuhrHxJfK2RSX4X0VePjWC0pbSgaoKF7Ot+UW+OQR8HQkvjMp70bATWjh8+StXXq8IHEaDOG9FAzsa5m8DSlvaGAkLgG2rNnyZJoFIzQurAs8BJkNI4fDpr1x5dYhcThpcbPK11QhKW1q4EBYA2xZGEMoCkZoXZiWeeyoQmXD49leuvDAF8a4x71ysINaEBfBhC6sC1JHwwq7IsQmHb3/lyqvTiK0l6dj5UvApVhBHwgJg22o18ulEa7hMVldirq/blKeOhNf6+UlUtKvgehRlb8TfsgWcJc2/pRSOOrC/cuW1FcRB/NucxxZzHmeNhZdGwqlIQbwQFsCHrVEhBDorx40IrjoSXpMou+52DNVQba8nUInfmiZZT+UjAn/lymsp36AQsjVeFKIom2d+FLu9EP/RsyD6tOU8zMMogA9brcOCAhZ88KqQyzFXCEzIemC/IvdXyrxs9xR2Fo0liFhdRibbmMQWKUpbu7jtGBk8FMCHrZvkt1SbUSnfoQqyPVtYkAnvmIC/SuOl2grmImBU8U1ia69Vqz1UZl+2psC9Dcp3qIMbQQ9RNmfZR+6vEnm5in0Rgrg35zR3rQG5MvuyJWv1KI6xUb5D18AZCGwsDUGs/iqVF4hy2K40FyGI74rMMSuzL1tnw97QMtGe4jvUgewUg8uRLd25siFyf5XIy+cc55iLILYarRlWZfZp66oRwMed88DLVpZTAu9QhUqxyo05jyVbdDhF7K9SeWHwy1oQq53Mt60ERmX2besm2R5RKxYYtnaOEfHaw3LZwzrp7H004eZzHiqUv0rlteDoyC9rQXzT7EFgVGbftvZ+pysU23SOhJcpX1mw6+5Vc926oTPsCuWvUnnJGu+7SKeSBbE2eCmulZnClk2wqVIT0TvUze9RRew83Btou+gQ0l8l8/J9ld+YuiDqDPOwKjOFLR+BOEb0DnXzW6ev1QkXqmGX7gp2CH+VygsU88oTwrxy0oJ4MZxsd6nMVLZGwx5TJ5nvWacmgndokp+MN9Ziyg1h2EXtr5J5XQiG5WOqgniwcLhtZaa0pQrEb8mh99HwzColL1l+/zZJVakmhIl6rE29lP4qmZfqkokOcJCsID5a5fpU/Ma2MlPaGh1aQdlm2ikwL1mZ201arpJXieJLBHvYKP1VKq/fRGKYrCDKjpZhV2ZKW6pAnByHoHVAXjZQbe8YAi46UPurVF4/FLfadICLJAVx7zRFp0h7BZg2z50C2VIFks5wY++EwX0zxKHmZYtKIYo2c1IN4pEvKn+VyOtALIZZCSJmGgLZUtnbuxBVd4WxDcjLBQ1ypZBxNxVyKn+Vxuug6KV24AcsiJEJ4tUxCJoMBREkPYUP5PsVTfc5UvmrJF6hxHAGC2Jkgnj2GIhNwoI4Ig0FsS8EoPJXKbxCiuEMFsTIBLFxDGzZUIUFUV7ZsOcjMf1VAq/nwGI4gwUxMkGsDJ41HepUifYQayQ7Pm5YpvJX7ryeFYtnFGKYrCAeNVZDsVZIKW3prPA9I71kal4/AeAJ8E8ofARadKD2V868niIRw2QF0RaUe+hcbJ0Vw5UnC+HoA/O6i6BvxTzRk6ZAqo5rnZA+muVyHDCkv3Lg1SueP6ySydcGH6Ha5LdNMpGS2aoC28paECuNTyweNs+rhKOJQBDX6UucRunESYSfq3QUQy7Vt3dNKrzPL8iF9FfqvGTDfddkei2aS+oD28paEHVf5qh5CB+TH5YgUgcG9ubuWPyVOi8WRGBB1MHc2v5FEo46Q0GU3chj8jEirMYihL9y4MWCCCyIYBDw345BiD0hHYMgmoihz0WH0P7KgRcLIrAgmgajTQs9IV/b7iqIuld7Yd+HqJoHw/5SHJW/cuHFgggsiKaoRQv7V1NUMC9RxRLEWlzr9Sl6G6YCOVjOibUP7ltc0h/wAwp/5cKrkfB4lEzE+BE6T7b6wLaKRSNe9J/VV+p6sSrbevjmiC+8rPZCvgk+S1r4vAq+qXDK2V+l8GIwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGA/LEf2oS4NVP9R70AAAAAElFTkSuQmCC") +``` +##### 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/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"