diff --git a/.gitignore b/.gitignore index 522eb87..ac8dbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ cookies *.settings logs +2captcha_settings.json +truecaptcha_settings.json # Byte-compiled / optimized / DLL files __pycache__/ @@ -162,4 +164,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +#.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 3b6ab51..289c741 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,11 @@ Start with run.py, or install with `pip install .` and run using roc-toolkit-cli If this is your first run, or you're missing settings files, they will be created for you -Auto captcha solving using [2Captcha](https://2captcha.com/) - +Auto captcha solving using [2Captcha](https://2captcha.com/) and [TrueCaptcha](https://truecaptcha.org/) Windows installation notes: `pip install .` should install everything - Ubuntu installation notes: Install pillow and ImageTK: `apt-get install python3-pil python3-pil.imagetk` @@ -20,53 +18,57 @@ Install lxml: `apt-get install python3-lxml` `pip install .` for the rest - ## User Settings -#### Login Details -**email:** email@email.com -**password:** password123 +#### Login Details +**email:** email@email.com +**password:** password123 -**auto_solve_captchas:** False or True -**auto_captcha_key:** replace_with_2captcha_apikey - +**auto_solve_captchas:** False or 2captcha or truecaptcha #### Range of time before checking for captchas + **min_checktime_secs:** 300 -**max_checktime_secs:** 600 +**max_checktime_secs:** 600 #### Switch to a second check range during a certain period of the day + **enable_nightmode:** False **nightmode_minwait_mins:** 60 **nightmode_maxwait_mins:** 120 -**nightmode_begin:** 20:00 (Must be a time in format HH:MM or HH:MM:SS) -**nightmode_end:** 09:00 (Must be a time in format HH:MM or HH:MM:SS) +**nightmode_begin:** 20:00 (Must be a time in format HH:MM or HH:MM:SS) +**nightmode_end:** 09:00 (Must be a time in format HH:MM or HH:MM:SS) + +#### Program exits/times out if this many failures occur consecutively -#### Program exits/times out if this many failures occur consecutively **max_consecutive_login_failures:** 2 **max_consecutive_captcha_attempts:** 3 (Actual failed captcha attempts) -**max_consecutive_answer_errors:** 5 (Receiving impossible answers i.e., letters) +**max_consecutive_answer_errors:** 5 (Receiving impossible answers i.e., letters) **captcha_failure_timeout:** 0 (How long to wait in minutes after failures. 0 Exits) -**captcha_save_path:** captcha_img/ (path to save captcha images to) +**captcha_save_path:** captcha_img/ (path to save captcha images to) #### Pull cookie from a browser you already use to login + **load_cookies_from_browser:** True or False **browser:** chrome or firefox #### Remote Lookup + **remote_captcha_lookup:** None (API url to lookup captcha answer based on hash) **remote_captcha_add:** None (API call to add captcha answer to database) ## Buyer Settings + Gold is divided by the folloing formula for each weapon: -(gold) * (weapon number) / (sum of all weapon numbers) +(gold) \* (weapon number) / (sum of all weapon numbers) ## Trainer settings + **train_soldiers:** True/False **soldier_weapon_match:** True/False (Should soldiers be purchased to match weapons) **soldier_dump_type:** attack/defense/spies/sentries (All excess untrained dumped to this category) **soldier_round_amount:** Integer > 0 (Instead of matching weaponcount exactly, round up this amount (nearest 100, 1000 etc) -**min_train_purchase_size:** Integer > 0 (Minimum size to complete a training purchase) +**min_train_purchase_size:** Integer > 0 (Minimum size to complete a training purchase) diff --git a/rocalert/__main__.py b/rocalert/__main__.py index b5e1051..3c63b1a 100644 --- a/rocalert/__main__.py +++ b/rocalert/__main__.py @@ -1,22 +1,32 @@ -from datetime import datetime, timedelta import random import time +from datetime import datetime, timedelta -from .roc_settings import SettingsError -from rocalert.pyrocalert import RocAlert -from rocalert.services.remote_lookup import RemoteCaptcha import rocalert.services.captchaservices as captchaservices -from rocalert.rocpurchases import ROCBuyer, SimpleRocTrainer -from rocalert.roc_settings import BuyerSettings,\ - SettingsSetupHelper, UserSettings, TrainerSettings from rocalert.captcha.captcha_logger import CaptchaLogger +from rocalert.pyrocalert import RocAlert +from rocalert.roc_settings import ( + BuyerSettings, + SettingsSetupHelper, + TrainerSettings, + UserSettings, +) +from rocalert.roc_settings._settings import is_negative_string from rocalert.roc_web_handler import RocWebHandler +from rocalert.rocpurchases import ROCBuyer, SimpleRocTrainer +from rocalert.services.remote_lookup import RemoteCaptcha from rocalert.services.urlgenerator import ROCDecryptUrlGenerator -from rocalert.services.useragentgenerator import UserAgentGenerator, Browser, OperatingSystem +from rocalert.services.useragentgenerator import ( + Browser, + OperatingSystem, + UserAgentGenerator, +) + +from .roc_settings import SettingsError -_user_settings_fp = 'user.settings' -_trainer_settings_fp = 'trainer.settings' -_buyer_settings_fp = 'buyer.settings' +_user_settings_fp = "user.settings" +_trainer_settings_fp = "trainer.settings" +_buyer_settings_fp = "buyer.settings" def _run(): @@ -28,14 +38,14 @@ def _run(): services = _configure_services(user_settings) a = RocAlert( - rochandler=services['rochandler'], + rochandler=services["rochandler"], usersettings=user_settings, - buyer=services['buyer'], - trainer=services['trainer'], - correctLog=services['correct_captcha_logger'], - generalLog=services['gen_captcha_logger'], - remoteCaptcha=services['remote_captcha'], - capsolver=services['capsolver'] + buyer=services["buyer"], + trainer=services["trainer"], + correctLog=services["correct_captcha_logger"], + generalLog=services["gen_captcha_logger"], + remoteCaptcha=services["remote_captcha"], + capsolver=services["capsolver"], ) a.start() @@ -43,9 +53,9 @@ def _run(): def _settings_are_valid() -> bool: filepaths = { - 'trainer': (_trainer_settings_fp, TrainerSettings), - 'user': (_user_settings_fp, UserSettings), - 'buyer': (_buyer_settings_fp, BuyerSettings) + "trainer": (_trainer_settings_fp, TrainerSettings), + "user": (_user_settings_fp, UserSettings), + "buyer": (_buyer_settings_fp, BuyerSettings), } settings_file_error = False @@ -54,8 +64,7 @@ def _settings_are_valid() -> bool: path, settingtype = infotuple if SettingsSetupHelper.needs_setup(path): settings_file_error = True - SettingsSetupHelper.create_default_file( - path, settingtype.DEFAULT_SETTINGS) + SettingsSetupHelper.create_default_file(path, settingtype.DEFAULT_SETTINGS) print(f"Created settings file {path}.") if settings_file_error: @@ -68,77 +77,107 @@ def _settings_are_valid() -> bool: def _configure_services(user_settings: UserSettings) -> dict[str, object]: services = {} - services['gen_captcha_logger'] = CaptchaLogger( - 'logs/captcha_answers.log', timestamp=True) + services["gen_captcha_logger"] = CaptchaLogger( + "logs/captcha_answers.log", timestamp=True + ) - services['correct_captcha_logger'] = CaptchaLogger( - 'logs/correct_ans.log', log_correctness=False) + services["correct_captcha_logger"] = CaptchaLogger( + "logs/correct_ans.log", log_correctness=False + ) - services['default_headers'] = _get_default_headers() + services["default_headers"] = _get_default_headers() - services['remote_captcha'] = RemoteCaptcha( - user_settings.get_value('remote_captcha_add'), - user_settings.get_value('remote_captcha_lookup')) + services["remote_captcha"] = RemoteCaptcha( + user_settings.get_value("remote_captcha_add"), + user_settings.get_value("remote_captcha_lookup"), + ) - services['urlgenerator'] = ROCDecryptUrlGenerator() + services["urlgenerator"] = ROCDecryptUrlGenerator() - if user_settings.get_setting('auto_solve_captchas').value: - savepath = user_settings.get_setting('captcha_save_path').value - apikey = user_settings.get_setting('auto_captcha_key').value - services['capsolver'] = captchaservices.TwocaptchaSolverService( - api_key=apikey, savepath=savepath) - else: - services['capsolver'] = captchaservices.ManualCaptchaSolverService() + services["capsolver"] = _get_captcha_solving_service(user_settings) - services['rochandler'] = RocWebHandler( - urlgenerator=services['urlgenerator'], - default_headers=services['default_headers']) + services["rochandler"] = RocWebHandler( + urlgenerator=services["urlgenerator"], + default_headers=services["default_headers"], + ) - services['buyer'] = ROCBuyer( - services['rochandler'], + services["buyer"] = ROCBuyer( + services["rochandler"], BuyerSettings(filepath=_buyer_settings_fp), ) - services['trainer'] = SimpleRocTrainer( + services["trainer"] = SimpleRocTrainer( TrainerSettings(filepath=_trainer_settings_fp) ) return services +def _get_captcha_solving_service(user_settings: UserSettings): + service = user_settings.get_setting("auto_solve_captchas").value.lower().strip() + + savepath = user_settings.get_setting("captcha_save_path").value + + if is_negative_string(service): + return captchaservices.ManualCaptchaSolverService() + + captcha_settings = captchaservices.get_captcha_settings(service) + if captcha_settings is None: + filename = captchaservices.create_captca_settings_file(service) + print(f"Created settings file {filename}. Please fill it out and restart") + quit() + + if service in ["2captcha", "twocaptcha"]: + apikey = captcha_settings["apiKey"] + return captchaservices.TwocaptchaSolverService( + api_key=apikey, savepath=savepath + ) + if service in ["truecaptcha", "true captcha"]: + return captchaservices.TrueCaptchaSolverService( + userid=captcha_settings["userId"], + api_key=captcha_settings["apiKey"], + mode=captcha_settings["mode"], + savepath=savepath, + ) + + def _get_default_headers(): - default_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' \ - + 'AppleWebKit/537.36 (KHTML, like Gecko) ' \ - + 'Chrome/114.0.0.0 Safari/537.36' + default_agent = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/114.0.0.0 Safari/537.36" + ) agentgenerator = UserAgentGenerator(default=default_agent) useragent = agentgenerator.get_useragent( - browser=Browser.Chrome, operatingsystem=OperatingSystem.Windows) + browser=Browser.Chrome, operatingsystem=OperatingSystem.Windows + ) print(f'Using user-agent: "{useragent}"') return { - 'Accept': 'text/html,application/xhtml+xml,application/xml' - + ';q=0.9,image/avif,image/webp,*/*;q=0.8', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive', - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'same-origin', - 'Sec-Fetch-User': '?1', - 'TE': 'trailers', - 'Upgrade-Insecure-Requests': '1', - 'User-Agent': useragent} + "Accept": "text/html,application/xhtml+xml,application/xml" + + ";q=0.9,image/avif,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Sec-Fetch-Site": "same-origin", + "Sec-Fetch-User": "?1", + "TE": "trailers", + "Upgrade-Insecure-Requests": "1", + "User-Agent": useragent, + } def _error_nap(errorcount, timebetweenerrors) -> None: muiltiplier = 1 if timebetweenerrors < timedelta(minutes=5): - print('Very recent error, increasing sleep time') + print("Very recent error, increasing sleep time") muiltiplier = 2 - base = 5*(max(1, errorcount % 4)) + base = 5 * (max(1, errorcount % 4)) sleeptime = int(muiltiplier * (base + random.uniform(0, 15))) - print(f'Sleeping for {sleeptime} minutes') - time.sleep(sleeptime*60) + print(f"Sleeping for {sleeptime} minutes") + time.sleep(sleeptime * 60) def main(): @@ -150,10 +189,10 @@ def main(): _run() keeprunning = False except KeyboardInterrupt as e: - print('Detected keyboard interrupt') + print("Detected keyboard interrupt") raise e except SettingsError as e: - print(f'Settings error: {e}\nExiting..') + print(f"Settings error: {e}\nExiting..") return except Exception as e: # TODO: Collect specific exceptions and handle them @@ -164,8 +203,8 @@ def main(): print(e) print(f"\nWarning: Detected exception #{errorcount}") _error_nap(errorcount, timebetweenerrors) - print('\n\nRestarting...') + print("\n\nRestarting...") -if __name__ == '__main__': +if __name__ == "__main__": exit(main()) diff --git a/rocalert/captcha/solvers/__init__.py b/rocalert/captcha/solvers/__init__.py index 1d1feb2..0a71c7f 100644 --- a/rocalert/captcha/solvers/__init__.py +++ b/rocalert/captcha/solvers/__init__.py @@ -1,6 +1,8 @@ from .manualcaptchasolver import manual_captcha_solve +from .truecaptchasolver import TrueCaptchaSolver from .twocaptchasolver import TwoCaptchaSolver if __name__ == "__main__": manual_captcha_solve() TwoCaptchaSolver() + TrueCaptchaSolver() diff --git a/rocalert/captcha/solvers/truecaptchasolver.py b/rocalert/captcha/solvers/truecaptchasolver.py new file mode 100644 index 0000000..d99bada --- /dev/null +++ b/rocalert/captcha/solvers/truecaptchasolver.py @@ -0,0 +1,36 @@ +import base64 + +import requests + +from rocalert.roc_web_handler import Captcha + + +class TrueCaptchaSolver: + def __init__(self, userid: str, api_key: str, api_url: str = None, mode="default"): + self._userid = userid + self._api_key = api_key + self._api_url = api_url or "https://api.apitruecaptcha.org/one/gettext" + self._mode = mode + + def solve(self, captcha: Captcha) -> str: + imageb64 = base64.b64encode(captcha.img).decode("ascii") + + data = { + "userid": self._userid, + "apikey": self._api_key, + "data": imageb64, + "tag": captcha.hash, + "numeric": True, + "len_str": 1, + "mode": "human", + } + + response = requests.post(url=self._api_url, json=data, timeout=40) + + if response.status_code != 200: + raise Exception("TrueCaptcha API returned non-200 status code") + + return response.json() + + def report_answer(self, captcha: Captcha): + pass diff --git a/rocalert/captcha/solvers/twocaptchasolver.py b/rocalert/captcha/solvers/twocaptchasolver.py index 7c49d52..381ba55 100644 --- a/rocalert/captcha/solvers/twocaptchasolver.py +++ b/rocalert/captcha/solvers/twocaptchasolver.py @@ -1,8 +1,10 @@ -from ...roc_web_handler import Captcha -from twocaptcha import TwoCaptcha -import PIL.Image -import io import collections +import io + +import PIL.Image +from twocaptcha import TwoCaptcha + +from ...roc_web_handler import Captcha class TwoCaptchaSolver: @@ -10,21 +12,23 @@ def __init__(self, api_key: str, savepath: str) -> None: self._solver = TwoCaptcha(apiKey=api_key) self._savepath = savepath self._lastresp = None - self._hinttext = 'A single digit between 1 and 9' + self._hinttext = "A single digit between 1 and 9" self._responsehistory = collections.deque(maxlen=10) def captcha_solve(self, captcha: Captcha): img = PIL.Image.open(io.BytesIO(captcha.img)) - path = f'{self._savepath}/{captcha.hash}.png' + path = f"{self._savepath}/{captcha.hash}.png" img.save(path) - response = self._solver.normal(path, hintText=self._hinttext) + response = self._solver.normal( + path, hintText=self._hinttext, numeric="1", minLen="1", maxLen="1" + ) self._responsehistory.append((captcha.hash, response)) - captcha.ans = response['code'] + captcha.ans = response["code"] return captcha def report_answer(self, captcha: Captcha): for chash, resp in self._responsehistory: if chash == captcha.hash: - self._solver.report(resp['captchaId'], captcha.ans_correct) + self._solver.report(resp["captchaId"], captcha.ans_correct) break diff --git a/rocalert/roc_settings/_settings.py b/rocalert/roc_settings/_settings.py index b215893..f969d25 100644 --- a/rocalert/roc_settings/_settings.py +++ b/rocalert/roc_settings/_settings.py @@ -1,26 +1,63 @@ import copy -from datetime import datetime as dt, time -from typing import Callable import os +from datetime import datetime as dt +from datetime import time +from typing import Callable + + +def is_negative_string(val: str) -> bool: + return val.lower().strip() in [ + "false", + "no", + "0", + "off", + "disable", + "disabled", + "none", + "nope", + "nah", + "n", + ] + + +def is_positive_string(val: str) -> bool: + return val.lower().strip() in [ + "true", + "yes", + "1", + "on", + "enable", + "enabled", + "yep", + "yup", + "yeah", + "y", + ] class SettingsError(Exception): pass -def time_conv(t: str): return dt.strptime(t, '%H:%M').time() if len( - t) <= 5 else dt.strptime(t, '%H:%M:%S').time() +def time_conv(t: str): + return ( + dt.strptime(t, "%H:%M").time() + if len(t) <= 5 + else dt.strptime(t, "%H:%M:%S").time() + ) class Setting: - def __init__(self, - prettyname: str, - name: str, - default_value=None, - valtype: type = None, - description: str = None, - value=None, - validation_func: Callable = None) -> None: + def __init__( + self, + prettyname: str, + name: str, + default_value=None, + valtype: type = None, + description: str = None, + value=None, + validation_func: Callable = None, + ) -> None: self.pname = prettyname self.name = name self.defaultval = default_value @@ -30,34 +67,38 @@ def __init__(self, self.validation_func = validation_func if valtype and type(valtype) != type: - print(f'Warning: {name} setting valtype is not a valid type.') + print(f"Warning: {name} setting valtype is not a valid type.") elif value and valtype and type(self.value) != valtype: - print(f'Warning: {name} setting type does not match value type.') + print(f"Warning: {name} setting type does not match value type.") class Settings: DEFAULT_SETTINGS = {} def __init__(self, name: str = None, filepath=None) -> None: - if not hasattr(self, 'settings'): + if not hasattr(self, "settings"): self.settings = {} # self.settings = copy.deepcopy(Settings.DEFAULT_SETTINGS) if name is None: - name = 'Settings' + name = "Settings" self.name = name self.mandatory = set() if filepath is not None: SettingsLoader.load_settings_from_path( - filepath, settings=self.settings, - default_settings=self.DEFAULT_SETTINGS, warnings=True) + filepath, + settings=self.settings, + default_settings=self.DEFAULT_SETTINGS, + warnings=True, + ) SettingsValidator.set_defaults_ifnotset( - self.settings, self.DEFAULT_SETTINGS) + self.settings, self.DEFAULT_SETTINGS + ) def load_settings_from_path(self, filepath) -> None: - SettingsLoader.load_settings_from_path( - filepath, self.settings, warnings=True) + SettingsLoader.load_settings_from_path(filepath, self.settings, warnings=True) SettingsValidator.check_mandatories( - self.settings, self.mandatory, quit_if_bad=True) + self.settings, self.mandatory, quit_if_bad=True + ) def get_setting(self, setting) -> Setting: if setting not in self.settings: @@ -69,7 +110,8 @@ def set_setting(self, setting, value) -> None: if setting in self.mandatory: SettingsValidator.check_mandatories( - self.settings, self.mandatory, quit_if_bad=True) + self.settings, self.mandatory, quit_if_bad=True + ) def get_value(self, settingname: str): return self.settings[settingname].value @@ -79,18 +121,18 @@ def print_settings_and_values(self, enumerate: bool = False) -> None: i = 0 for settingid, setting in self.settings.items(): if enumerate: - print('{}. '.format(i), end='') + print("{}. ".format(i), end="") - print('{} : {}'.format(settingid, setting)) + print("{} : {}".format(settingid, setting)) def print_settings(self, enumerate: bool = False) -> None: print(self.name) i = 0 for setting, value in self.settings.items(): if enumerate: - print('{}. '.format(i), end='') + print("{}. ".format(i), end="") - print('{} : {}'.format(setting, value)) + print("{} : {}".format(setting, value)) def get_settings(self) -> dict[str, Setting]: return self.settings @@ -112,7 +154,7 @@ def __tostr__(value: str) -> str: return value def __toint__(value: str) -> int: - return int(value.replace(',', '').replace(' ', '')) + return int(value.replace(",", "").replace(" ", "")) def __totime__(value: str) -> time: return time_conv(value) @@ -122,7 +164,7 @@ def __tofloat__(value: str) -> float: def __tobool__(value: str) -> bool: value = value.lower() - validans = ['yes', 'y', 'true', 'enable', 'enabled'] + validans = ["yes", "y", "true", "enable", "enabled"] return value in validans __convmap__ = { @@ -131,18 +173,19 @@ def __tobool__(value: str) -> bool: time: __totime__, bool: __tobool__, float: __tofloat__, - } + } class SettingsLoader: - def load_settings_from_path(filepath, - settings: dict[str, Setting], - default_settings: dict[str, Setting], - warnings: bool = False - ) -> dict: + def load_settings_from_path( + filepath, + settings: dict[str, Setting], + default_settings: dict[str, Setting], + warnings: bool = False, + ) -> dict: if settings is None: if warnings: - print('Warning. Empty settings dict passed to loader') + print("Warning. Empty settings dict passed to loader") settings = {} with open(filepath) as f: @@ -151,48 +194,48 @@ def load_settings_from_path(filepath, for line in lines: line = SettingsLoader.__split_comment(line) - if ':' not in line and len(line.strip()) > 0: + if ":" not in line and len(line.strip()) > 0: if warnings: - print(f'Warning: found non-setting line {line}') + print(f"Warning: found non-setting line {line}") continue - setting_name, value = line.split(':', maxsplit=1) + setting_name, value = line.split(":", maxsplit=1) setting_name = setting_name.strip() value = value.strip() - if warnings and setting_name == '': + if warnings and setting_name == "": print("Warning: A setting existed with no name") continue if warnings and len(value) == 0: print("Warning: setting {} has no value".format(setting_name)) continue if setting_name not in default_settings: - settings[setting_name] = Setting( - setting_name, setting_name) + settings[setting_name] = Setting(setting_name, setting_name) if warnings: - print(f"Warning: Setting {setting_name} found " - + "that is not in defaults") + print( + f"Warning: Setting {setting_name} found " + + "that is not in defaults" + ) else: - settings[setting_name] = copy.copy( - default_settings[setting_name]) + settings[setting_name] = copy.copy(default_settings[setting_name]) setting = settings[setting_name] try: - setting.value = SettingsConverter.convert( - value, setting.valtype) + setting.value = SettingsConverter.convert(value, setting.valtype) except ValueError: raise SettingsError( - f'Error converting setting {setting.pname} with ' - + f'value {value} to type {setting.valtype.__name__}') + f"Error converting setting {setting.pname} with " + + f"value {value} to type {setting.valtype.__name__}" + ) return settings def __split_comment(line: str) -> str: - return line.split('#', maxsplit=1)[0] + return line.split("#", maxsplit=1)[0] class SettingsSaver: def __make_str(key, val) -> str: - return '{}:{}\n'.format(key, val) + return "{}:{}\n".format(key, val) def __make_lines(settings: dict): arr = [] @@ -203,57 +246,55 @@ def __make_lines(settings: dict): def save_settings_toPath(filepath: str, settings: dict) -> None: lines = SettingsSaver.__make_lines(settings) - with open(filepath, 'w') as f: + with open(filepath, "w") as f: f.writelines(lines) class SettingsValidator: def __check_dict_generic( - setdic: dict[str, Setting], - key: str, - default: Setting, - callback: Callable - ) -> None: + setdic: dict[str, Setting], key: str, default: Setting, callback: Callable + ) -> None: if key not in setdic: - print(f"Warning: setting {key} not in settings." - + f" Set to default value: {default.value}") + print( + f"Warning: setting {key} not in settings." + + f" Set to default value: {default.value}" + ) setdic[key] = copy.copy(default) elif callback: setdic[key] = callback(setdic[key]) - def validate_set(settings: dict, - settings_to_validate, - validationfunc: Callable - ) -> bool: + def validate_set( + settings: dict, settings_to_validate, validationfunc: Callable + ) -> bool: for item in settings_to_validate: if item not in settings or not validationfunc(settings[item]): return False return True - def set_defaults_ifnotset(settings: dict[str, Setting], - defaults: dict[str, Setting], - callback: callable = None - ) -> None: + def set_defaults_ifnotset( + settings: dict[str, Setting], + defaults: dict[str, Setting], + callback: callable = None, + ) -> None: if settings is None or defaults is None: return for key, val in defaults.items(): - SettingsValidator.__check_dict_generic( - settings, key, val, callback) + SettingsValidator.__check_dict_generic(settings, key, val, callback) # Return true if all mandatories are set - def check_mandatories(settings: dict[str, Setting], - mandatories, - printError: bool = True, - quit_if_bad=False - ) -> bool: + def check_mandatories( + settings: dict[str, Setting], + mandatories, + printError: bool = True, + quit_if_bad=False, + ) -> bool: if settings is None or mandatories is None: return False errorcount = 0 for setting in mandatories: - if setting not in settings\ - or settings[setting] is None: + if setting not in settings or settings[setting] is None: if printError: print("ERROR: {} setting not set!".format(setting)) errorcount += 1 @@ -264,9 +305,8 @@ def check_mandatories(settings: dict[str, Setting], return errorcount == 0 def check_settings_in_range( - settings: dict[str, Setting], - warnings: bool = False) -> bool: - + settings: dict[str, Setting], warnings: bool = False + ) -> bool: valid = True for settingid, setting in settings.items(): curvalid = True @@ -276,7 +316,7 @@ def check_settings_in_range( curvalid &= setting.validation_func(setting.value) if warnings and not curvalid: - print(f'Warning: Settings {settingid} value is invalid') + print(f"Warning: Settings {settingid} value is invalid") valid &= curvalid return valid diff --git a/rocalert/roc_settings/_usersettings.py b/rocalert/roc_settings/_usersettings.py index 94b746c..612416b 100644 --- a/rocalert/roc_settings/_usersettings.py +++ b/rocalert/roc_settings/_usersettings.py @@ -1,90 +1,174 @@ -from ._settings import Setting, Settings, SettingsValidator, time_conv -from datetime import time import os +from datetime import time + +from ._settings import ( + Setting, + Settings, + SettingsValidator, + is_negative_string, + time_conv, +) + +VALID_CAPTCHA_SERVICES = {"twocaptcha", "2captcha", "true captcha", "truecaptcha"} + + +def is_valid_captcha_service(service: str) -> bool: + cleaned_service = service.lower().strip() + + return service in VALID_CAPTCHA_SERVICES or is_negative_string(cleaned_service) class UserSettings(Settings): DEFAULT_SETTINGS = { - 'email': Setting('Email Address', 'email', 'email@address.com', str, - 'ROC login email'), - 'password': Setting('Password', 'password', 'password', str, - 'ROC login password'), - 'auto_solve_captchas': - Setting('Auto Solve Captchas', - 'auto_solve_captchas', False, bool, - 'Automatically solve captchas using a service'), - 'auto_captcha_key': Setting('Autosolve Captcha API Key', - 'auto_captcha_key', None, str, - 'API key for captcha solving service'), - 'min_checktime_secs': - Setting('Minimum check time', 'min_checktime_secs', 1000, int, - 'Minimum seconds to wait before an account status check'), - 'max_checktime_secs': - Setting('Maximum check time', 'max_checktime_secs', 2000, int, - 'Maximum seconds to wait before an account status check'), - 'enable_nightmode': - Setting('Enable nightmode', 'enable_nightmode', False, bool, - 'Enable longer wait times during certain time period'), - 'nightmode_minwait_mins': - Setting('Nightmode minimum wait time', 'nightmode_minwait_mins', - 100, float, 'Minimum MINUTES to wait during nightmode'), - 'nightmode_maxwait_mins': - Setting('Nightmode maxmimum wait time', 'nightmode_maxwait_mins', - 200, float, 'Maximum MINUTE to wait during nightmode'), - 'nightmode_begin': - Setting('Nightmode start time', 'nightmode_begin', - time_conv('20:00'), time, - 'Start time of nightmode format HH:MM:SS'), - 'nightmode_end': - Setting('Nightmode end time', 'nightmode_end', - time_conv('08:00'), time, - 'End time of nightmode, formatted HH:MM:SS'), - 'max_consecutive_login_failures': - Setting('Max repeated login attempts', - 'max_consecutive_login_failures', 2, int, - 'Max login attempt before terminating program'), - 'max_consecutive_captcha_attempts': - Setting('Max repeated captcha attempts', - 'max_consecutive_captcha_attempts', 5, int, - 'Max attempts of a captcha before timing out or exiting'), - 'max_consecutive_answer_errors': - Setting('Max repeated bad captcha answers', - 'max_consecutive_answer_errors', 5, int, - 'Maximum bad answers to receive before giving up' - + '(Not Attempts)'), - 'captcha_save_path': - Setting('Captcha save path', 'captcha_save_path', r'captcha_img/', - str, 'Path to save captcha images to'), - 'load_cookies_from_browser': - Setting('Load cookies from browser', 'load_cookies_from_browser', - False, bool, 'Attempt to retrieve cookies from browser'), - 'browser': - Setting('Browser choice', 'browser', 'all', str, - 'Browser to load cookies from', - None, - lambda x: x in ['all', 'chrome', 'firefox', 'opera', 'edge' - 'chromium', 'brave', 'vivaldi', 'safari']), - 'remote_captcha_lookup': - Setting('Remote captcha lookup API address', - 'remote_captcha_lookup', None, str, - 'URL to API for captcha answer lookup'), - 'remote_captcha_add': - Setting('Remote captcha add API address', 'remote_captcha_add', - None, str, 'URL to API to add captcha answer'), - 'captcha_failure_timeout': - Setting('Captcha failure timeout length', - 'captcha_failure_timeout', 0, int, - 'Amount of time to wait after captcha error limit' - + ' reached. 0 to exit instead of timeout') + "email": Setting( + "Email Address", "email", "email@address.com", str, "ROC login email" + ), + "password": Setting( + "Password", "password", "password", str, "ROC login password" + ), + "auto_solve_captchas": Setting( + "Captcha Solving Service", + "auto_solve_captchas", + "none", + str, + "Service to use to solve captchas", + "none", + is_valid_captcha_service, + ), + "min_checktime_secs": Setting( + "Minimum check time", + "min_checktime_secs", + 1000, + int, + "Minimum seconds to wait before an account status check", + ), + "max_checktime_secs": Setting( + "Maximum check time", + "max_checktime_secs", + 2000, + int, + "Maximum seconds to wait before an account status check", + ), + "enable_nightmode": Setting( + "Enable nightmode", + "enable_nightmode", + False, + bool, + "Enable longer wait times during certain time period", + ), + "nightmode_minwait_mins": Setting( + "Nightmode minimum wait time", + "nightmode_minwait_mins", + 100, + float, + "Minimum MINUTES to wait during nightmode", + ), + "nightmode_maxwait_mins": Setting( + "Nightmode maxmimum wait time", + "nightmode_maxwait_mins", + 200, + float, + "Maximum MINUTE to wait during nightmode", + ), + "nightmode_begin": Setting( + "Nightmode start time", + "nightmode_begin", + time_conv("20:00"), + time, + "Start time of nightmode format HH:MM:SS", + ), + "nightmode_end": Setting( + "Nightmode end time", + "nightmode_end", + time_conv("08:00"), + time, + "End time of nightmode, formatted HH:MM:SS", + ), + "max_consecutive_login_failures": Setting( + "Max repeated login attempts", + "max_consecutive_login_failures", + 2, + int, + "Max login attempt before terminating program", + ), + "max_consecutive_captcha_attempts": Setting( + "Max repeated captcha attempts", + "max_consecutive_captcha_attempts", + 5, + int, + "Max attempts of a captcha before timing out or exiting", + ), + "max_consecutive_answer_errors": Setting( + "Max repeated bad captcha answers", + "max_consecutive_answer_errors", + 5, + int, + "Maximum bad answers to receive before giving up" + "(Not Attempts)", + ), + "captcha_save_path": Setting( + "Captcha save path", + "captcha_save_path", + r"captcha_img/", + str, + "Path to save captcha images to", + ), + "load_cookies_from_browser": Setting( + "Load cookies from browser", + "load_cookies_from_browser", + False, + bool, + "Attempt to retrieve cookies from browser", + ), + "browser": Setting( + "Browser choice", + "browser", + "all", + str, + "Browser to load cookies from", + None, + lambda x: x + in [ + "all", + "chrome", + "firefox", + "opera", + "edge" "chromium", + "brave", + "vivaldi", + "safari", + ], + ), + "remote_captcha_lookup": Setting( + "Remote captcha lookup API address", + "remote_captcha_lookup", + None, + str, + "URL to API for captcha answer lookup", + ), + "remote_captcha_add": Setting( + "Remote captcha add API address", + "remote_captcha_add", + None, + str, + "URL to API to add captcha answer", + ), + "captcha_failure_timeout": Setting( + "Captcha failure timeout length", + "captcha_failure_timeout", + 0, + int, + "Amount of time to wait after captcha error limit" + + " reached. 0 to exit instead of timeout", + ), } def __init__(self, name: str = None, filepath=None) -> None: if name is None: - name = 'User Settings' + name = "User Settings" super().__init__(name, filepath) - self.mandatory = {'email', 'password'} + self.mandatory = {"email", "password"} if filepath is not None: self.__check_valid_settings() @@ -95,40 +179,47 @@ def load_settings_from_path(self, filepath) -> None: def __check_valid_settings(self): SettingsValidator.check_mandatories( - self.settings, self.mandatory, quit_if_bad=True) + self.settings, self.mandatory, quit_if_bad=True + ) inrange = SettingsValidator.check_settings_in_range( - self.settings, warnings=True) + self.settings, warnings=True + ) if not inrange: print("ERROR: User settings are invalid!") quit() - savepath = self.get_value('captcha_save_path') + savepath = self.get_value("captcha_save_path") if not os.path.exists(savepath): - print(f'Warning: path {savepath} does not exist.' - + ' Creating directories.') + print( + f"Warning: path {savepath} does not exist." + " Creating directories." + ) os.makedirs(savepath) def auto_solve_captchas(self) -> bool: - return self.get_value('auto_solve_captchas') + return self.get_value("auto_solve_captchas") def auto_solve_api_key(self) -> str: - return self.get_value('auto_captcha_key') + return self.get_value("auto_captcha_key") @property - def use_nightmode(self) -> bool: return self.settings['enable_nightmode'] + def use_nightmode(self) -> bool: + return self.settings["enable_nightmode"] @property def nightmode_waittime_range(self) -> tuple[float, float]: - return (self.get_setting('nightmode_minwait_mins'), - self.get_setting('nightmode_maxwait_mins')) + return ( + self.get_setting("nightmode_minwait_mins"), + self.get_setting("nightmode_maxwait_mins"), + ) @property def nightmode_activetime_range(self) -> tuple[time, time]: - return (self.get_setting('nightmode_begin'), - self.get_setting('nightmode_end')) + return (self.get_setting("nightmode_begin"), self.get_setting("nightmode_end")) @property def regular_waittimes_seconds(self) -> tuple[int, int]: - return (self.get_setting['min_checktime_secs'], - self.get_setting['max_checktime_secs']) + return ( + self.get_setting["min_checktime_secs"], + self.get_setting["max_checktime_secs"], + ) diff --git a/rocalert/roc_web_handler.py b/rocalert/roc_web_handler.py index 3bfe8eb..4dabe91 100644 --- a/rocalert/roc_web_handler.py +++ b/rocalert/roc_web_handler.py @@ -250,7 +250,11 @@ def submit_equation(self, captcha: Captcha, page: str = "roc_recruit") -> bool: return self.__page_captcha_type() == Captcha.CaptchaType.IMAGE def _check_incorrect_captcha(self) -> bool: - return "Wrong number" not in self.r.text or "wrong number" in self.r.text + return not ( + "Wrong number" in self.r.text + or "wrong number" in self.r.text + or "You hit the wrong number. " in self.r.text + ) def submit_captcha_url( self, captcha: Captcha, url: str, payload: dict = None, manual_page: str = None diff --git a/rocalert/rocpurchases/__init__.py b/rocalert/rocpurchases/__init__.py index e2ca6a4..9b7d711 100644 --- a/rocalert/rocpurchases/__init__.py +++ b/rocalert/rocpurchases/__init__.py @@ -1,12 +1,17 @@ from .roc_buyer import ROCBuyer from .rocpurchtools import RocItem, RocItemGroup, ALL_ITEM_DETAILS -from .roc_trainer import ROCTrainingPurchaseCreatorABC,\ - ROCTrainingPayloadCreatorABC, ROCTrainingPayloadCreator,\ - ROCTrainingDumpPurchaseCreator, ROCTrainingWeaponMatchPurchaseCreator,\ - ROCTrainerABC, SimpleRocTrainer +from .roc_trainer import ( + ROCTrainingPurchaseCreatorABC, + ROCTrainingPayloadCreatorABC, + ROCTrainingPayloadCreator, + ROCTrainingDumpPurchaseCreator, + ROCTrainingWeaponMatchPurchaseCreator, + ROCTrainerABC, + SimpleRocTrainer, +) -if __name__ == '__main__': - print('don\'t run this file') +if __name__ == "__main__": + print("don't run this file") ROCBuyer() RocItem() ROCTrainingPurchaseCreatorABC() diff --git a/rocalert/services/captchaservices.py b/rocalert/services/captchaservices.py index 8875171..7f20272 100644 --- a/rocalert/services/captchaservices.py +++ b/rocalert/services/captchaservices.py @@ -1,8 +1,15 @@ import abc +import io +import json +import os +from typing import Optional + +import PIL.Image from requests import Response -from rocalert.roc_web_handler import RocWebHandler -from rocalert.roc_web_handler import Captcha -from ..captcha.solvers import manual_captcha_solve, TwoCaptchaSolver + +from rocalert.roc_web_handler import Captcha, RocWebHandler + +from ..captcha.solvers import TrueCaptchaSolver, TwoCaptchaSolver, manual_captcha_solve class CaptchaSolveException(Exception): @@ -29,13 +36,13 @@ def run_service(cls, response: Response, roc: RocWebHandler) -> Captcha: Returns: Captcha: _description_ Captcha pulled from page. - Will be None if no captcha is detected + Will be None if no captcha is detected.. """ if roc is None: - raise Exception('ROC parameter must exist') + raise Exception("ROC parameter must exist") elif response is None: - raise Exception('Response parameter must exist') + raise Exception("Response parameter must exist") captcha_type = cls._detectcaptchatype(response) @@ -50,11 +57,11 @@ def run_service(cls, response: Response, roc: RocWebHandler) -> Captcha: @classmethod def _get_img_hash(cls, pagetext: str) -> str: - index = pagetext.find('img.php?hash=') + index = pagetext.find("img.php?hash=") if index == -1: return None - endIndex = pagetext.find('"', index, index+100) - imghash = pagetext.text[index + len('img.php?hash='): endIndex] + endIndex = pagetext.find('"', index, index + 100) + imghash = pagetext.text[index + len("img.php?hash=") : endIndex] return imghash @classmethod @@ -65,29 +72,29 @@ def _get_img_captcha(cls, resp: Response, roc: RocWebHandler) -> Captcha: @classmethod def _get_equation_captcha(cls, resp: Response) -> Captcha: - index = resp.text.find('