#!/usr/bin/env python3 import os, sys import time import requests from base64 import b64encode try: from .api import ApiClient except ImportError: from api import ApiClient class SolverExceptions(Exception): pass class ValidationException(SolverExceptions): pass class NetworkException(SolverExceptions): pass class ApiException(SolverExceptions): pass class TimeoutException(SolverExceptions): pass class Solvecaptcha(): def __init__(self, apiKey, softId=4844, callback=None, defaultTimeout=120, recaptchaTimeout=600, pollingInterval=10, server = 'api.solvecaptcha.com', extendedResponse=None): self.API_KEY = apiKey self.soft_id = softId self.callback = callback self.default_timeout = defaultTimeout self.recaptcha_timeout = recaptchaTimeout self.polling_interval = pollingInterval self.api_client = ApiClient(post_url = str(server)) self.max_files = 9 self.exceptions = SolverExceptions self.extendedResponse = extendedResponse def recaptcha(self, sitekey, url, version='v2', enterprise=0, **kwargs): '''Wrapper for solving recaptcha (v2, v3). Parameters _______________ sitekey : str Value of sitekey parameter you found on page. url : str Full URL of the page where you see the reCAPTCHA. domain : str, optional Domain used to load the captcha: google.com or recaptcha.net. Default: google.com. invisible : int, optional 1 - means that reCAPTCHA is invisible. 0 - normal reCAPTCHA. Default: 0. version : str, optional v3 — defines that you're sending a reCAPTCHA V3. Default: v2. enterprise : str, optional 1 - defines that you're sending reCAPTCHA Enterpise. Default: 0. action : str, optional Value of action parameter you found on page. Default: verify. score : str, only for v3, optional The score needed for resolution. Currently, it's almost impossible to get token with score higher than 0.3. Default: 0.4. cookies : str, only for v2, optional Your cookies that will be passed to our worker who solve the captha. We also return worker's cookies in the response if you use json=1. Format: KEY:Value, separator: semicolon, example: KEY1:Value1;KEY2:Value2; userAgent : str, only for v2, optional Your userAgent that will be passed to our worker and used to solve the captcha. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' params = { 'googlekey': sitekey, 'url': url, 'method': 'userrecaptcha', 'version': version, 'enterprise': enterprise, **kwargs, } result = self.solve(timeout=self.recaptcha_timeout, **params) return result def hcaptcha(self, sitekey, url, **kwargs): '''Wrapper for solving hcaptcha. Parameters __________ sitekey : str Value of data-sitekey parameter you found on page. url : str Full URL of the page where you bypass the captcha. invisible : num, optional Use 1 for invisible version of hcaptcha. Currently it is a very rare case. Default: 0. data : str, optional Custom data that is used in some implementations of hCaptcha, mostly with invisible=1. In most cases you see it as rqdata inside network requests. Format: "data": "rqDataValue". domain : str, optional Domain used to load the captcha: hcaptcha.com or js.hcaptcha.com. Default: hcaptcha.com. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' result = self.solve(sitekey=sitekey, url=url, method='hcaptcha', **kwargs) return result def turnstile(self, sitekey, url, **kwargs): '''Wrapper for solving Cloudflare Turnstile. Parameters __________ sitekey : str Value of sitekey parameter you found on page. url : str Full URL of the page where you see the captcha. useragent : str User-Agent of your browser. Must match the User-Agent you use to access the site. Use only modern browsers released within the last 6 months. action : str. optional Value of optional action parameter you found on page, can be defined in data-action attribute or passed to turnstile.render call. data : str, optional The value of cData passed to turnstile.render call. Also can be defined in data-cdata attribute. pagedata : str, optional The value of the chlPageData parameter when calling turnstile.render. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' result = self.solve(sitekey=sitekey, url=url, method='turnstile', **kwargs) return result def geetest(self, gt, challenge, url, **kwargs): '''Wrapper for solving geetest captcha. Parameters: __________ gt : str Value of gt parameter you found on target website. challenge : str Value of challenge parameter you found on target website. url : str Full URL of the page where you see Geetest captcha. offline : num, optional In rare cases initGeetest can be called with offline parameter. If the call uses offline: true, set the value to 1. Default: 0. new_captcha : num, optional In rare cases initGeetest can be called with new_captcha parameter. If the call uses new_captcha: true, set the value to 1. Mostly used with offline parameter. userAgent : str, optional Your userAgent that will be passed to our worker and used to solve the captcha. apiServer : str, optional Value of api_server parameter you found on target website. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' result = self.solve(gt=gt, challenge=challenge, url=url, method='geetest', **kwargs) return result def geetest_v4(self, captcha_id, url, **kwargs): '''Wrapper for solving geetest_v4 captcha. Parameters __________ captcha_id : str Value of captcha_id parameter you found on target website. url: str Full URL of the page where you see Geetest captcha. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' result = self.solve(captcha_id=captcha_id, url=url, method='geetest_v4', **kwargs) return result def normal(self, file, **kwargs): '''Wrapper for solving a normal captcha (image). Parameters __________ file : file Captcha image file. * required if you submit image as a file (method=post). body : str Base64-encoded captcha image. * required if you submit image as Base64-encoded string (method=base64). phrase : int, optional 0 - captcha contains one word. 1 - captcha contains two or more words. Default: 0. numeric : int, optional 0 - not specified. 1 - captcha contains only numbers. 2 - captcha contains only letters. 3 - captcha contains only numbers OR only letters. 4 - captcha MUST contain both numbers AND letters. Default: 0 minLen : int, optional 0 - not specified. 1..20 - minimal number of symbols in captcha. Default: 0. maxLen : int, optional 0 - not specified. 1..20 - maximal number of symbols in captcha. Default: 0. caseSensitive : int, optional 0 - captcha in not case sensitive. 1 - captcha is case sensitive. Default: 0. calc : int, optional 0 - not specified. 1 - captcha requires calculation (e.g. type the result 4 + 8 = ). Default: 0. lang : str, optional Language code. See the list of supported languages https://solvecaptcha.com/captcha-solver-api#language. hintText : str, optional Max 140 characters. Endcoding: UTF-8. Text will be shown to worker to help him to solve the captcha correctly. For example: type red symbols only. hintImg : img, optional Max 400x150px, 100 kB. Image with instruction for solving reCAPTCHA. Not required if you're sending instruction as text with textinstructions. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. ''' method = self.get_method(file) result = self.solve(**method, **kwargs) return result def text(self, text, **kwargs): '''Wrapper for solving text captcha. Parameters __________ text : str Max 140 characters. Endcoding: UTF-8. Text will be shown to worker to help him to solve the captcha correctly. For example: type red symbols only. lang: str, optional Language code. See the list of supported languages https://solvecaptcha.com/captcha-solver-api#language. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. ''' result = self.solve(text=text, method='post', **kwargs) return result def funcaptcha(self, sitekey, url, **kwargs): '''Wrapper for solving funcaptcha. Parameters __________ sitekey : str Value of pk or data-pkey parameter you found on page. url : str Full URL of the page where you see the FunCaptcha. surl : str, optional Value of surl parameter you found on page. userAgent: str, optional Tells us to use your user-agent value. data[key] : str, optional Custom data to pass to FunCaptcha. For example: data[blob]=stringValue. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' result = self.solve(publickey=sitekey, url=url, method='funcaptcha', **kwargs) return result def keycaptcha(self, s_s_c_user_id, s_s_c_session_id, s_s_c_web_server_sign, s_s_c_web_server_sign2, url, **kwargs): '''Wrapper for solving. Parameters __________ s_s_c_user_id : str Value of s_s_c_user_id parameter you found on page. s_s_c_session_id : str Value of s_s_c_session_id parameter you found on page. s_s_c_web_server_sign : str Value of s_s_c_web_server_sign parameter you found on page. s_s_c_web_server_sign2 : str Value of s_s_c_web_server_sign2 parameter you found on page. url : str Full URL of the page where you see the KeyCaptcha. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' params = { 's_s_c_user_id': s_s_c_user_id, 's_s_c_session_id': s_s_c_session_id, 's_s_c_web_server_sign': s_s_c_web_server_sign, 's_s_c_web_server_sign2': s_s_c_web_server_sign2, 'url': url, 'method': 'keycaptcha', **kwargs, } result = self.solve(**params) return result def grid(self, file, **kwargs): '''Wrapper for solving grid captcha (image). Required: file : file Captcha image file. * required if you submit image as a file (method=post). body : str Base64-encoded captcha image. * required if you submit image as Base64-encoded string (method=base64). hintText : str Max 140 characters. Endcoding: UTF-8. Text with instruction for solving reCAPTCHA. For example: select images with trees. Not required if you're sending instruction as an image with imginstructions. hintImg : img Max 400x150px, 100 kB. Image with instruction for solving reCAPTCHA. Not required if you're sending instruction as text with textinstructions. rows : int, optional Number of rows in reCAPTCHA grid. cols : itn, optional Number of columns in reCAPTCHA grid. previousId : str, optional Id of your previous request with the same captcha challenge. canSkip : int, optional 0 - not specified. 1 - possibly there's no images that fit the instruction. Set the value to 1 only if it's possible that there's no images matching to the instruction. We'll provide a button "No matching images" to worker, and you will receive No_matching_images as answer. Default: 0. lang: str, optional Language code. See the list of supported languages https://solvecaptcha.com/captcha-solver-api#language. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' method = self.get_method(file) params = { 'recaptcha': 1, **method, **kwargs, } result = self.solve(**params) return result def coordinates(self, file, **kwargs): '''Wrapper for solving coordinates captcha (image). Parameters __________ file : file Captcha image file. * required if you submit image as a file (method=post). body : str Base64-encoded captcha image. * required if you submit image as Base64-encoded string (method=base64). hintText : str Max 140 characters. Endcoding: UTF-8. Text with instruction for solving the captcha. For example: click on images with ghosts. Not required if the image already contains the instruction. hintImg : img Max 400x150px, 100 kB. Image with instruction for solving reCAPTCHA. Not required if you're sending instruction as text with textinstructions. lang : str, optional Language code. See the list of supported languages https://solvecaptcha.com/captcha-solver-api#language. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. ''' method = self.get_method(file) params = { 'coordinatescaptcha': 1, **method, **kwargs, } result = self.solve(**params) return result def rotate(self, files, **kwargs): '''Wrapper for solving rotate captcha (image). Parameters __________ files : file Captcha image file. * required if you submit image as a file (method=post). body : str Base64-encoded captcha image. * required if you submit image as Base64-encoded string (method=base64). angle : int, optional Angle for one rotation step in degrees. If not defined we'll use the default value for FunCaptcha: 40 degrees. Default: 40. lang : str, optional Language code. See the list of supported languages https://solvecaptcha.com/captcha-solver-api#language. hintImg : str, optional Image with instruction for worker to help him to solve captcha correctly. hintText : str, optional Text will be shown to worker to help him to to solve captcha correctly. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. proxy : dict, optional {'type': 'HTTPS', 'uri': 'login:password@IP_address:PORT'}. ''' if isinstance(files, str): file = self.get_method(files)['file'] result = self.solve(file=file, method='rotatecaptcha', **kwargs) return result elif isinstance(files, dict): files = list(files.values()) files = self.extract_files(files) result = self.solve(files=files, method='rotatecaptcha', **kwargs) return result def canvas(self, file, **kwargs): '''Wrapper for solving canvas captcha (image). Parameters __________ file : file Captcha image file. * required if you submit image as a file (method=post). body : str Base64-encoded captcha image. * required if you submit image as Base64-encoded string (method=base64). hintText : str Max 140 characters. Endcoding: UTF-8. Text with instruction for solving reCAPTCHA. For example: select images with trees. Not required if you're sending instruction as an image with imginstructions. hintImg : img Max 400x150px, 100 kB. Image with instruction for solving reCAPTCHA. Not required if you're sending instruction as text with textinstructions. canSkip : int, optional 0 - not specified. 1 - possibly there's no images that fit the instruction. Set the value to 1 only if it's possible that there's no images matching to the instruction. We'll provide a button "No matching images" to worker, and you will receive No_matching_images as answer. Default: 0. lang : int, optional 0 - not specified. 1 - Cyrillic captcha. 2 - Latin captcha. Default: 0. callback : str, optional URL for pingback (callback) response that will be sent when captcha is solved. URL should be registered on the server. More info here https://solvecaptcha.com/captcha-solver-api#manage_pingback. ''' if not ('hintText' in kwargs or 'hintImg' in kwargs): raise ValidationException( 'parameters required: hintText and/or hintImg') method = self.get_method(file) params = { 'recaptcha': 1, 'canvas': 1, **method, **kwargs, } result = self.solve(**params) return result def solve(self, timeout=0, polling_interval=0, **kwargs): '''Sends captcha, receives result. Parameters __________ timeout : float polling_interval : int **kwargs : dict all captcha params Returns result : string ''' id_ = self.send(**kwargs) result = {'captchaId': id_} if self.callback is None: timeout = float(timeout or self.default_timeout) sleep = int(polling_interval or self.polling_interval) code = self.wait_result(id_, timeout, sleep) if self.extendedResponse == True: new_code = { key if key != 'request' else 'code': value for key, value in code.items() if key != 'status' } result.update(new_code) else: result.update({'code': code}) return result def wait_result(self, id_, timeout, polling_interval): max_wait = time.time() + timeout while time.time() < max_wait: try: return self.get_result(id_) except NetworkException: time.sleep(polling_interval) raise TimeoutException(f'timeout {timeout} exceeded') def get_method(self, file): if not file: raise ValidationException('File required') if not '.' in file and len(file) > 50: return {'method': 'base64', 'body': file} if file.startswith('http'): img_resp = requests.get(file) if img_resp.status_code != 200: raise ValidationException(f'File could not be downloaded from url: {file}') return {'method': 'base64', 'body': b64encode(img_resp.content).decode('utf-8')} if not os.path.exists(file): raise ValidationException(f'File not found: {file}') return {'method': 'post', 'file': file} def send(self, **kwargs): """This method can be used for manual captcha submission Parameters _________ method : str The name of the method must be found in the documentation https://solvecaptcha.com/captcha-solver-api kwargs: dict All captcha params Returns """ params = self.default_params(kwargs) params = self.rename_params(params) params, files = self.check_hint_img(params) response = self.api_client.in_(files=files, **params) if not response.startswith('OK|'): raise ApiException(f'cannot recognize response {response}') return response[3:] def get_result(self, id_): import json """This method can be used for manual captcha answer polling. Parameters __________ id_ : str ID of the captcha sent for solution Returns answer : text """ if self.extendedResponse == True: response = self.api_client.res(key=self.API_KEY, action='get', id=id_, json=1) response_data = json.loads(response) if response_data.get("status") == 0: raise NetworkException if not response_data.get("status") == 1: raise ApiException(f'Unexpected status in response: {response_data}') return response_data else: response = self.api_client.res(key=self.API_KEY, action='get', id=id_) if response == 'CAPCHA_NOT_READY': raise NetworkException if not response.startswith('OK|'): raise ApiException(f'cannot recognize response {response}') return response[3:] def balance(self): '''Get my balance Returns balance : float ''' response = self.api_client.res(key=self.API_KEY, action='getbalance') return float(response) def report(self, id_, correct): '''Report of solved captcha: good/bad. Parameters __________ id_ : str captcha ID correct : bool True/False Returns None. ''' rep = 'reportgood' if correct else 'reportbad' self.api_client.res(key=self.API_KEY, action=rep, id=id_) return def rename_params(self, params): replace = { 'caseSensitive': 'regsense', 'minLen': 'min_len', 'maxLen': 'max_len', 'minLength': 'min_len', 'maxLength': 'max_len', 'hintText': 'textinstructions', 'hintImg': 'imginstructions', 'url': 'pageurl', 'score': 'min_score', 'text': 'textcaptcha', 'rows': 'recaptcharows', 'cols': 'recaptchacols', 'previousId': 'previousID', 'canSkip': 'can_no_answer', 'apiServer': 'api_server', 'softId': 'soft_id', 'callback': 'pingback', 'datas': 'data-s', } new_params = { v: params.pop(k) for k, v in replace.items() if k in params } proxy = params.pop('proxy', '') proxy and new_params.update({ 'proxy': proxy['uri'], 'proxytype': proxy['type'] }) new_params.update(params) return new_params def default_params(self, params): params.update({'key': self.API_KEY}) callback = params.pop('callback', self.callback) soft_id = params.pop('softId', self.soft_id) if callback: params.update({'callback': callback}) if soft_id: params.update({'softId': soft_id}) self.has_callback = bool(callback) return params def extract_files(self, files): if len(files) > self.max_files: raise ValidationException( f'Too many files (max: {self.max_files})') not_exists = [f for f in files if not (os.path.exists(f))] if not_exists: raise ValidationException(f'File not found: {not_exists}') files = {f'file_{e+1}': f for e, f in enumerate(files)} return files def check_hint_img(self, params): hint = params.pop('imginstructions', None) files = params.pop('files', {}) if not hint: return params, files if not '.' in hint and len(hint) > 50: params.update({'imginstructions': hint}) return params, files if not os.path.exists(hint): raise ValidationException(f'File not found: {hint}') if not files: files = {'file': params.pop('file', {})} files.update({'imginstructions': hint}) return params, files if __name__ == '__main__': key = sys.argv[1] sol = Solvecaptcha(key)