diff --git a/.gitignore b/.gitignore index 4d44f24..e3e013f 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,5 @@ dmypy.json .pyre/ .idea +.DS_Store proxies_extension.zip \ No newline at end of file diff --git a/README.md b/README.md index 58e90fa..79cebd8 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,19 @@ This project automates solving Google reCAPTCHA v2 with image challenges (3x3 an ## Features -- Uses **Selenium WebDriver** to interact with the browser and manipulate elements on the reCAPTCHA page. -- **2Captcha API** helps solve image-based captchas using artificial intelligence. +- **Selenium WebDriver**: Interacts with the browser and manipulates elements on the reCAPTCHA page. +- **2Captcha API**: Solves image-based captchas using artificial intelligence. - Handles both **3x3** and **4x4** captchas with custom logic for each. -- Tracks image updates and handles captcha error messages efficiently. +- Modular design with separated logic into helper classes for easy code maintenance and future expansion. +- Tracks image updates and handles captcha error messages efficiently using custom error handling. + +## Code Structure + +The project is structured as follows: + +- **`utils/actions.py`**: Contains the `PageActions` class, which encapsulates common browser actions (clicking, switching frames, etc.). +- **`utils/helpers.py`**: Contains the `CaptchaHelper` class, responsible for solving captchas, executing JS, and handling captcha error messages. +- **`js_scripts/`**: JavaScript files that extract captcha data and track image updates. ## Usage @@ -43,16 +52,28 @@ python solve_recaptcha.py ## How It Works -1. Browser Initialization: A browser is opened using Selenium WebDriver. -2. Captcha Data Retrieval: JavaScript extracts the image tiles from reCAPTCHA and sends them to the 2Captcha service for solving. -3. Captcha Submission: Once a solution is received from 2Captcha, Selenium simulates clicking on the correct image tiles based on the solution. -4. Captcha Submission: The solution is submitted once the captcha is solved. +1. **Browser Initialization:** A browser is opened using Selenium WebDriver. +2. **Captcha Data Retrieval:** JavaScript extracts the image tiles from reCAPTCHA and sends them to the 2Captcha service for solving. +3. **Captcha Submission:** Once a solution is received from 2Captcha, Selenium simulates clicking on the correct image tiles based on the solution. +4. **Final Submission:** The solution is submitted once the captcha is solved. ## Captcha Solving Logic -- For 3x3 captchas, the previous captcha ID (previousID) is saved to speed up solving when images are updated. -- For 4x4 captchas, no previousID is saved, and each solution is processed from scratch. -- Error messages, such as “Please try again” are handled, and the solving process is retried if needed. +- **3x3 Captchas:** Previous captcha ID (previousID) is saved to speed up solving when images are updated. +- **4x4 Captchas:** No previousID is saved, and each solution is processed from scratch. +- **Error Handling:** Messages like “Please try again” are handled, and the solving process is retried if needed. + +## Modular Design + +The project follows a modular design for better maintainability: + +- **PageActions Class:** Handles general browser interactions like switching to iframes, clicking elements, and returning focus to the main content. +- **CaptchaHelper Class:** Encapsulates captcha-specific logic, such as solving the captcha via 2Captcha API, handling error messages, and executing JavaScript in the browser. + +## JavaScript Scripts + +- `get_captcha_data.js`: Extracts captcha image tiles for solving. The source code of the script is located here https://gist.github.com/kratzky/20ea5f4f142cec8f1de748b3f3f84bfc +- `track_image_updates.js`: Monitors requests to check if captcha images are updated. [2captcha-demo]: https://2captcha.com/demo diff --git a/js_scripts/get_captcha_data.js b/js_scripts/get_captcha_data.js new file mode 100644 index 0000000..7027908 --- /dev/null +++ b/js_scripts/get_captcha_data.js @@ -0,0 +1,53 @@ +window.getCaptchaData = () => { + return new Promise((resolve, reject) => { + let canvas = document.createElement('canvas'); + let ctx = canvas.getContext('2d'); + let comment = document.querySelector('.rc-imageselect-desc-wrapper').innerText.replace(/\n/g, ' '); + + let img4x4 = document.querySelector('img.rc-image-tile-44'); + if (!img4x4) { + let table3x3 = document.querySelector('table.rc-imageselect-table-33 > tbody'); + if (!table3x3) { + reject('Can not find reCAPTCHA elements'); + } + + let initial3x3img = table3x3.querySelector('img.rc-image-tile-33'); + + canvas.width = initial3x3img.naturalWidth; + canvas.height = initial3x3img.naturalHeight; + ctx.drawImage(initial3x3img, 0, 0); + + let updatedTiles = document.querySelectorAll('img.rc-image-tile-11'); + + if (updatedTiles.length > 0) { + const pos = [ + { x: 0, y: 0 }, { x: ctx.canvas.width / 3, y: 0 }, { x: ctx.canvas.width / 3 * 2, y: 0 }, + { x: 0, y: ctx.canvas.height / 3 }, { x: ctx.canvas.width / 3, y: ctx.canvas.height / 3 }, { x: ctx.canvas.width / 3 * 2, y: ctx.canvas.height / 3 }, + { x: 0, y: ctx.canvas.height / 3 * 2 }, { x: ctx.canvas.width / 3, y: ctx.canvas.height / 3 * 2 }, { x: ctx.canvas.width / 3 * 2, y: ctx.canvas.height / 3 * 2 } + ]; + updatedTiles.forEach((t) => { + const ind = t.parentElement.parentElement.parentElement.tabIndex - 3; + ctx.drawImage(t, pos[ind - 1].x, pos[ind - 1].y); + }); + } + resolve({ + rows: 3, + columns: 3, + type: 'GridTask', + comment, + body: canvas.toDataURL().replace(/^data:image\/?[A-z]*;base64,/, '') + }); + } else { + canvas.width = img4x4.naturalWidth; + canvas.height = img4x4.naturalHeight; + ctx.drawImage(img4x4, 0, 0); + resolve({ + rows: 4, + columns: 4, + comment, + body: canvas.toDataURL().replace(/^data:image\/?[A-z]*;base64,/, ''), + type: 'GridTask' + }); + } + }); +}; \ No newline at end of file diff --git a/js_scripts/track_image_updates.js b/js_scripts/track_image_updates.js new file mode 100644 index 0000000..2ee7bc1 --- /dev/null +++ b/js_scripts/track_image_updates.js @@ -0,0 +1,22 @@ +window.monitorRequests = () => { + let found = false; + + const observer = new PerformanceObserver((list) => { + const entries = list.getEntries(); + entries.forEach((entry) => { + if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') { + const url = new URL(entry.name); + if (url.href.includes("recaptcha/api2/replaceimage")) { + found = true; // If the request is found, set the flag to true + } + } + }); + }); + + observer.observe({ entryTypes: ['resource'] }); + + // We return the result after 10 seconds + return new Promise((resolve) => { + setTimeout(() => resolve(found), 10000); + }); +}; \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4a382cd --- /dev/null +++ b/main.py @@ -0,0 +1,139 @@ +import time +from selenium import webdriver +import os +from twocaptcha import TwoCaptcha +from utils.actions import PageActions +from utils.helpers import CaptchaHelper + +# CONFIGURATION +url = "https://2captcha.com/demo/recaptcha-v2" +apikey = os.getenv('APIKEY_2CAPTCHA') # Get the API key for the 2Captcha service from environment variables +solver = TwoCaptcha(apikey) + +# LOCATORS +l_iframe_captcha = "//iframe[@title='reCAPTCHA']" +l_checkbox_captcha = "//span[@role='checkbox']" +l_popup_captcha = "//iframe[contains(@title, 'two minutes')]" +l_verify_button = "//button[@id='recaptcha-verify-button']" +l_submit_button_captcha = "//button[@type='submit']" +l_try_again = "//div[@class='rc-imageselect-incorrect-response']" +l_select_more = "//div[@class='rc-imageselect-error-select-more']" +l_dynamic_more = "//div[@class='rc-imageselect-error-dynamic-more']" +l_select_something = "//div[@class='rc-imageselect-error-select-something']" + +# MAIN LOGIC +options = webdriver.ChromeOptions() +options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'}) + +with webdriver.Chrome(options=options) as browser: + browser.get(url) + print("Started") + + # Instantiate helper classes + page_actions = PageActions(browser) + captcha_helper = CaptchaHelper(browser, solver) + + # We start by clicking on the captcha checkbox + page_actions.switch_to_iframe(l_iframe_captcha) + page_actions.click_checkbox(l_checkbox_captcha) + page_actions.switch_to_default_content() + page_actions.switch_to_iframe(l_popup_captcha) + time.sleep(1) + + # Load JS files + script_get_data_captcha = captcha_helper.load_js_script('js_scripts/get_captcha_data.js') + script_change_tracking = captcha_helper.load_js_script('js_scripts/track_image_updates.js') + + # Inject JS once + captcha_helper.execute_js(script_get_data_captcha) + captcha_helper.execute_js(script_change_tracking) + + id = None # Initialize the id variable for captcha + + while True: + # Get captcha data by calling the JS function directly + captcha_data = browser.execute_script("return getCaptchaData();") + + # Forming parameters for solving captcha + params = { + "method": "base64", + "img_type": "recaptcha", + "recaptcha": 1, + "cols": captcha_data['columns'], + "rows": captcha_data['rows'], + "textinstructions": captcha_data['comment'], + "lang": "en", + "can_no_answer": 1 + } + + # If the 3x3 captcha is an id, add previousID to the parameters + if params['cols'] == 3 and id: + params["previousID"] = id + + print("Params before solving captcha:", params) + + # Send captcha for solution + result = captcha_helper.solver_captcha(file=captcha_data['body'], **params) + + if result is None: + print("Captcha solving failed or timed out. Stopping the process.") + break + + # Check if the captcha was solved successfully + elif result and 'no_matching_images' not in result['code']: + # We save the id only on the first successful iteration for 3x3 captcha + if id is None and params['cols'] == 3 and result['captchaId']: + id = result['captchaId'] # Save id for subsequent iterations + + answer = result['code'] + number_list = captcha_helper.pars_answer(answer) + + # Processing for 3x3 + if params['cols'] == 3: + # Click on the answers found + page_actions.clicks(number_list) + + # Check if the images have been updated + image_update = page_actions.check_for_image_updates() + + if image_update: + # If the images have been updated, continue with the saved id + print(f"Images updated, continuing with previousID: {id}") + continue # Continue the loop + + # Press the check button after clicks + page_actions.click_check_button(l_verify_button) + + # Processing for 4x4 + elif params['cols'] == 4: + # Click on the answers found and immediately press the check button + page_actions.clicks(number_list) + page_actions.click_check_button(l_verify_button) + + # After clicking, we check for errors and image updates + image_update = page_actions.check_for_image_updates() + + if image_update: + print(f"Images updated, continuing without previousID") + continue # Continue the loop + + # If the images are not updated, check the error messages + if captcha_helper.handle_error_messages(l_try_again, l_select_more, l_dynamic_more, l_select_something): + continue # If an error is visible, restart the loop + + # If there are no errors, send the captcha + page_actions.switch_to_default_content() + page_actions.click_check_button(l_submit_button_captcha) + break # Exit the loop if the captcha is solved + + elif 'no_matching_images' in result['code']: + # If the captcha returned the code "no_matching_images", check the errors + page_actions.click_check_button(l_verify_button) + if captcha_helper.handle_error_messages(l_try_again, l_select_more, l_dynamic_more, l_select_something): + continue # Restart the loop if an error is visible + else: + page_actions.switch_to_default_content() + page_actions.click_check_button(l_submit_button_captcha) + break # Exit loop + + time.sleep(10) \ No newline at end of file diff --git a/recaptcha_grid.py b/recaptcha_grid.py deleted file mode 100644 index 8011ee1..0000000 --- a/recaptcha_grid.py +++ /dev/null @@ -1,324 +0,0 @@ -import time -from selenium import webdriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -import os -from twocaptcha import TwoCaptcha - -# CONFIGURATION -url = "https://2captcha.com/demo/recaptcha-v2" -apikey = os.getenv('APIKEY_2CAPTCHA') # Get the API key for the 2Captcha service from environment variables -solver = TwoCaptcha(apikey) - -# JavaScript script to get captcha data -script_get_data_captcha = """ -const getCaptchaData = () => { - return new Promise((resolve, reject)=>{ - let canvas = document.createElement('canvas') - let ctx = canvas.getContext('2d') - let comment = document.querySelector('.rc-imageselect-desc-wrapper').innerText.replace(/\\n/g, ' ') - - let img4x4 = document.querySelector('img.rc-image-tile-44') - if (!img4x4) { - let table3x3 = document.querySelector('table.rc-imageselect-table-33 > tbody') - if (!table3x3) { - reject('Can not find reCAPTCHA elements') - } - - let initial3x3img = table3x3.querySelector('img.rc-image-tile-33') - - canvas.width = initial3x3img.naturalWidth - canvas.height = initial3x3img.naturalHeight - ctx.drawImage(initial3x3img, 0, 0) - - let updatedTiles = document.querySelectorAll('img.rc-image-tile-11') - - if (updatedTiles.length > 0) { - const pos = [ - { x: 0, y: 0 }, { x: ctx.canvas.width / 3, y: 0 }, { x: ctx.canvas.width / 3 * 2, y: 0 }, - { x: 0, y: ctx.canvas.height / 3 }, { x: ctx.canvas.width / 3, y: ctx.canvas.height / 3 }, { x: ctx.canvas.width / 3 * 2, y: ctx.canvas.height / 3 }, - { x: 0, y: ctx.canvas.height / 3 * 2 }, { x: ctx.canvas.width / 3, y: ctx.canvas.height / 3 * 2 }, { x: ctx.canvas.width / 3 * 2, y: ctx.canvas.height / 3 * 2 } - ] - updatedTiles.forEach((t) => { - const ind = t.parentElement.parentElement.parentElement.tabIndex - 3 - ctx.drawImage(t, pos[ind - 1].x, pos[ind - 1].y) - }) - } - resolve({ - rows: 3, - columns: 3, - type: 'GridTask', - comment, - body: canvas.toDataURL().replace(/^data:image\/?[A-z]*;base64,/, '') - }) - } else { - canvas.width = img4x4.naturalWidth - canvas.height = img4x4.naturalHeight - ctx.drawImage(img4x4, 0, 0) - resolve({ - rows: 4, - columns: 4, - comment, - body: canvas.toDataURL().replace(/^data:image\/?[A-z]*;base64,/, ''), - type: 'GridTask' - }) - } - }) -}; -return getCaptchaData(); -""" - -# JavaScript to track network requests to update images -script_change_tracking = """ -const monitorRequests = () => { - let found = false; - - const observer = new PerformanceObserver((list) => { - const entries = list.getEntries(); - entries.forEach((entry) => { - if (entry.initiatorType === 'xmlhttprequest' || entry.initiatorType === 'fetch') { - const url = new URL(entry.name); - if (url.href.includes("recaptcha/api2/replaceimage")) { - found = true; // If the request is found, set the flag to true - } - } - }); - }); - - observer.observe({ entryTypes: ['resource'] }); - - // We return the result after 10 seconds - return new Promise((resolve) => { - setTimeout(() => resolve(found), 10000); - }); -}; - -return monitorRequests(); -""" - -# LOCATORS -locator_iframe_captcha = "//iframe[@title='reCAPTCHA']" -locator_checkbox_captcha = "//span[@role='checkbox']" -locator_popup_captcha = "//iframe[contains(@title, 'two minutes')]" -locator_verify_button = "//button[@id='recaptcha-verify-button']" -locator_submit_button_captcha_locator = "//button[@type='submit']" -locator_try_again = "//div[@class='rc-imageselect-incorrect-response']" -locator_select_more = "//div[@class='rc-imageselect-error-select-more']" -locator_dynamic_more = "//div[@class='rc-imageselect-error-dynamic-more']" -locator_select_something = "//div[@class='rc-imageselect-error-select-something']" - -# GETTERS -def get_clickable_element(locator, timeout=30): - """Waits until the element is clickable and returns it.""" - return WebDriverWait(browser, timeout).until(EC.element_to_be_clickable((By.XPATH, locator))) - - -def get_presence_element(locator, timeout=30): - """Waits until the element appears in the DOM and returns it.""" - return WebDriverWait(browser, timeout).until(EC.presence_of_element_located((By.XPATH, locator))) - - -# ACTIONS -def switch_to_iframe(iframe_locator): - """Switches focus to the iframe of the captcha.""" - iframe = get_presence_element(iframe_locator) - browser.switch_to.frame(iframe) - print("Switched to captcha widget") - - -def click_checkbox(checkbox_locator): - """Clicks on the reCAPTCHA checkbox.""" - checkbox = get_clickable_element(checkbox_locator) - checkbox.click() - print("Checked the checkbox") - - -def execute_js(script): - """Executes JavaScript code in the browser.""" - return browser.execute_script(script) - - -def switch_to_default_content(): - """Returns focus to the main page content.""" - browser.switch_to.default_content() - print("Returned focus to the main page content") - - - -def solver_captcha(**kwargs): - """Solves reCAPTCHA using the 2Captcha service.""" - try: - result = solver.grid(**kwargs) - print(f"Captcha solved") - return result - except Exception as e: - print(f"An error occurred: {e}") - return None - - -def pars_answer(answer): - """Parses the response from 2Captcha and returns a list of numbers for clicks.""" - numbers_str = answer.split(":")[1] - number_list = list(map(int, numbers_str.split("/"))) - new_number_list = [i + 3 for i in number_list] - print("Parsed the response to a list of cell numbers") - return new_number_list - - -def clicks(answer_list): - """Clicks on image cells based on a list of numbers.""" - for i in answer_list: - get_presence_element(f"//table//td[@tabindex='{i}']").click() - print("Сells are marked") - - -def is_message_visible(locator): - """Checks the visibility of an element with a captcha error message""" - try: - element = get_presence_element(locator) - is_visible = browser.execute_script(""" - var elem = arguments[0]; - var style = window.getComputedStyle(elem); - return !(style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0'); - """, element) - return is_visible - except Exception as e: - print(f"Error: {e}") - return False - -def handle_error_messages(): - """ - Checks for error messages on the captcha and returns True if they are visible. - """ - time.sleep(1) - if is_message_visible(locator_try_again): - return True - elif is_message_visible(locator_select_more): - return True - elif is_message_visible(locator_dynamic_more): - return True - elif is_message_visible(locator_select_something): - return True - print("No error messages") - return False - -def click_check_button(locator): - """Clicks on the captcha check button""" - time.sleep(1) - get_clickable_element(locator).click() - print("Pressed the Check button") - - -def check_for_image_updates(script): - """Checks captcha images update via JavaScript.""" - print("Images updated") - return execute_js(script) - - -# MAIN LOGIC -options = webdriver.ChromeOptions() -options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'}) - -# Basic program logic -with webdriver.Chrome(options=options) as browser: - browser.get(url) - print("Started") - - # We start by clicking on the captcha checkbox - switch_to_iframe(locator_iframe_captcha) - click_checkbox(locator_checkbox_captcha) - switch_to_default_content() - time.sleep(1) - switch_to_iframe(locator_popup_captcha) - time.sleep(1) - - id = None # Initialize the id variable for captcha - - while True: - # Get captcha data - captcha_data = execute_js(script_get_data_captcha) - - # Forming parameters for solving captcha - params = { - "method": "base64", - "img_type": "recaptcha", - "recaptcha": 1, - "cols": captcha_data['columns'], - "rows": captcha_data['rows'], - "textinstructions": captcha_data['comment'], - "lang": "en", - "can_no_answer": 1 - } - - # If the 3x3 captcha is an id, add previousID to the parameters - if params['cols'] == 3 and id: - params["previousID"] = id - - print("Params before solving captcha:", params) - - # Send captcha for solution - result = solver_captcha(file=captcha_data['body'], **params) - - if result is None: - print("Captcha solving failed or timed out. Stopping the process.") - break - - # Check if the captcha was solved successfully - elif result and 'no_matching_images' not in result['code']: - # We save the id only on the first successful iteration for 3x3 captcha - if id is None and params['cols'] == 3 and result['captchaId']: - id = result['captchaId'] # Save id for subsequent iterations - - answer = result['code'] - number_list = pars_answer(answer) - - # Processing for 3x3 - if params['cols'] == 3: - # Click on the answers found - clicks(number_list) - - # Check if the images have been updated - image_update = check_for_image_updates(script_change_tracking) - - if image_update: - # If the images have been updated, continue with the saved id - print(f"Images updated, continuing with previousID: {id}") - continue # Continue the loop - - # Press the check button after clicks - click_check_button(locator_verify_button) - - # Processing for 4x4 - elif params['cols'] == 4: - # Click on the answers found and immediately press the check button - clicks(number_list) - click_check_button(locator_verify_button) - - # After clicking, we check for errors and image updates - image_update = check_for_image_updates(script_change_tracking) - - if image_update: - print(f"Images updated, continuing without previousID") - continue # Continue the loop - - # If the images are not updated, check the error messages - if handle_error_messages(): - continue # If an error is visible, restart the loop - - # If there are no errors, send the captcha - switch_to_default_content() - click_check_button(locator_submit_button_captcha_locator) - break # Exit the loop if the captcha is solved - - elif 'no_matching_images' in result['code']: - # If the captcha returned the code "no_matching_images", check the errors - click_check_button(locator_verify_button) - if handle_error_messages(): - continue # Restart the loop if an error is visible - else: - switch_to_default_content() - click_check_button(locator_submit_button_captcha_locator) - break # Exit loop - print("Finished") - time.sleep(10) \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/actions.py b/utils/actions.py new file mode 100644 index 0000000..0441e44 --- /dev/null +++ b/utils/actions.py @@ -0,0 +1,91 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import time + +class PageActions: + """ + The PageActions class provides methods for interacting with page elements via Selenium WebDriver. + Used to perform actions such as switching to an iframe, clicking on elements and checking their state. + """ + def __init__(self, browser): + """ + Initializing PageActions. + + :param browser: Selenium WebDriver object for interacting with the browser. + """ + self.browser = browser + + def get_clickable_element(self, locator, timeout=30): + """ + Waits until the element is clickable and returns it. + + :param locator: XPath element locator. + :param timeout: Timeout in seconds (default 30). + :return: Clickable element. + """ + return WebDriverWait(self.browser, timeout).until(EC.element_to_be_clickable((By.XPATH, locator))) + + def get_presence_element(self, locator, timeout=30): + """ + Waits until the element appears in the DOM and returns it. + + :param locator: XPath element locator. + :param timeout: Timeout in seconds (default 30). + :return: Found element. + """ + return WebDriverWait(self.browser, timeout).until(EC.presence_of_element_located((By.XPATH, locator))) + + def switch_to_iframe(self, iframe_locator): + """ + Switches focus to the iframe of the captcha. + + :param iframe_locator: XPath locator of the iframe. + """ + iframe = self.get_presence_element(iframe_locator) + self.browser.switch_to.frame(iframe) + print("Switched to captcha widget") + + def click_checkbox(self, checkbox_locator): + """ + Clicks on the checkbox element of the captcha. + + :param checkbox_locator: XPath locator of the captcha checkbox + """ + checkbox = self.get_clickable_element(checkbox_locator) + checkbox.click() + print("Checked the checkbox") + + def switch_to_default_content(self): + """Returns focus to the main page content from the iframe.""" + self.browser.switch_to.default_content() + print("Returned focus to the main page content") + + def clicks(self, answer_list): + """ + Clicks on the image cells in the captcha in accordance with the transmitted list of cell numbers. + + :param answer_list: List of cell numbers to click. + """ + for i in answer_list: + self.get_presence_element(f"//table//td[@tabindex='{i}']").click() + print("Cells are marked") + + def click_check_button(self, locator): + """ + Clicks on the "Check" button on the captcha after selecting images. + + :param locator: XPath locator for the "Check" button. + """ + time.sleep(1) + self.get_clickable_element(locator).click() + print("Pressed the Check button") + + def check_for_image_updates(self): + """ + Checks if captcha images have been updated using JavaScript. + + :return: Returns True if the images have been updated, False otherwise. + """ + print("Images updated") + return self.browser.execute_script("return monitorRequests();") \ No newline at end of file diff --git a/utils/helpers.py b/utils/helpers.py new file mode 100644 index 0000000..d7a1e2d --- /dev/null +++ b/utils/helpers.py @@ -0,0 +1,105 @@ +import time + +from utils.actions import PageActions + +class CaptchaHelper: + """ + The CaptchaHelper class provides methods for interacting with captchas + and executing JavaScript code through Selenium WebDriver. Interaction + with captchas is carried out using the 2Captcha service. + """ + def __init__(self, browser, solver): + """ + Initializing CaptchaHelper. + + :param browser: Selenium WebDriver object for interacting with the browser. + :param solver: 2Captcha object for solving captchas. + """ + self.browser = browser + self.solver = solver + self.page_actions = PageActions(browser) + + def execute_js(self, script): + """Executes JavaScript code in the browser. + + :param script: A string of JavaScript code to be executed in the context of the current page. + :return: The result of JavaScript execution. + """ + print("Executing JS") + return self.browser.execute_script(script) + + def solver_captcha(self, **kwargs): + """Sends the captcha image to be solved via the 2Captcha service + + :param kwargs: Additional parameters for 2Captcha (for example, base64 image). + :return: The result of solving the captcha or None in case of an error. + """ + try: + result = self.solver.grid(**kwargs) + print(f"Captcha solved") + return result + except Exception as e: + print(f"An error occurred: {e}") + return None + + def pars_answer(self, answer): + """Parses the response from 2Captcha and returns a list of numbers for clicks. + + :param answer: Response from 2Captcha in string format (e.g. "OK: 1/2/3"). + :return: List of cell numbers to click. + """ + numbers_str = answer.split(":")[1] + number_list = list(map(int, numbers_str.split("/"))) + new_number_list = [i + 3 for i in number_list] # Add 3 to go to the correct index. + print("Parsed the response to a list of cell numbers") + return new_number_list + + def is_message_visible(self, locator): + """Checks the visibility of an element with a captcha error message + + :param locator: XPath locator of the element to check. + :return: True if the element is visible, otherwise False. + """ + try: + element = self.page_actions.get_presence_element(locator) + is_visible = self.browser.execute_script(""" + var elem = arguments[0]; + var style = window.getComputedStyle(elem); + return !(style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0'); + """, element) + return is_visible + except Exception as e: + print(f"Error: {e}") + return False + + def handle_error_messages(self, l_try_again, l_select_more, l_dynamic_more, l_select_something): + """ + Checks for error messages on the captcha and returns True if they are visible. + + :param l_try_again: Locator for the "Try again" message. + :param l_select_more: Locator for the "Select more" message. + :param l_dynamic_more: Locator for dynamic error. + :param l_select_something: Locator for the "Select something" message. + :return: True if any of the error messages are visible, otherwise False. + """ + time.sleep(1) + if self.is_message_visible(l_try_again): + return True + elif self.is_message_visible(l_select_more): + return True + elif self.is_message_visible(l_dynamic_more): + return True + elif self.is_message_visible(l_select_something): + return True + print("No error messages") + return False + + def load_js_script(self, file_path): + """ + Loads JavaScript code from a file. + + :param file_path: Path to the file with JavaScript code. + :return: A string containing the contents of the file. + """ + with open(file_path, 'r') as file: + return file.read() \ No newline at end of file