From 9f3590b0a0d3c16d5fa46e22078189f1cdf90cb7 Mon Sep 17 00:00:00 2001 From: Xewdy <95155966+Xewdy444@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:45:01 -0600 Subject: [PATCH] Stability improvements --- .../recaptchav2/async_solver.py | 102 +++++++++++------- .../recaptchav2/recaptcha_box.py | 6 +- .../recaptchav2/sync_solver.py | 91 +++++++++------- 3 files changed, 121 insertions(+), 78 deletions(-) diff --git a/playwright_recaptcha/recaptchav2/async_solver.py b/playwright_recaptcha/recaptchav2/async_solver.py index 4c21daa..99933ef 100644 --- a/playwright_recaptcha/recaptchav2/async_solver.py +++ b/playwright_recaptcha/recaptchav2/async_solver.py @@ -177,18 +177,6 @@ async def _random_delay(self, short: bool = True) -> None: delay_time = random.randint(150, 350) if short else random.randint(1250, 1500) await self._page.wait_for_timeout(delay_time) - async def _wait_for_value(self, attribute: str) -> None: - """ - Wait for an attribute to have a value. - - Parameters - ---------- - attribute : str - The attribute. - """ - while getattr(self, attribute) is None: - await self._page.wait_for_timeout(250) - async def _get_capsolver_response( self, recaptcha_box: AsyncRecaptchaBox, image_data: bytes ) -> Optional[Dict[str, Any]]: @@ -359,7 +347,7 @@ async def _click_checkbox(self, recaptcha_box: AsyncRecaptchaBox) -> None: raise RecaptchaRateLimitError if await recaptcha_box.challenge_is_visible(): - break + return await self._page.wait_for_timeout(250) @@ -387,12 +375,10 @@ async def _get_audio_url(self, recaptcha_box: AsyncRecaptchaBox) -> str: raise RecaptchaRateLimitError if await recaptcha_box.audio_challenge_is_visible(): - break + return await recaptcha_box.audio_download_button.get_attribute("href") await self._page.wait_for_timeout(250) - return await recaptcha_box.audio_download_button.get_attribute("href") - async def _submit_audio_text( self, recaptcha_box: AsyncRecaptchaBox, text: str ) -> None: @@ -415,10 +401,10 @@ async def _submit_audio_text( async with self._page.expect_response( re.compile("/recaptcha/(api2|enterprise)/userverify") - ): + ) as response: await recaptcha_box.verify_button.click() - await self._wait_for_value("_token") + await response.value while recaptcha_box.frames_are_attached(): if await recaptcha_box.rate_limit_is_visible(): @@ -429,7 +415,47 @@ async def _submit_audio_text( or await recaptcha_box.solve_failure_is_visible() or await recaptcha_box.challenge_is_solved() ): - break + return + + await self._page.wait_for_timeout(250) + + async def _submit_tile_answers(self, recaptcha_box: AsyncRecaptchaBox) -> None: + """ + Submit the reCAPTCHA image challenge tile answers. + + Parameters + ---------- + recaptcha_box : AsyncRecaptchaBox + The reCAPTCHA box. + + Raises + ------ + RecaptchaRateLimitError + If the reCAPTCHA rate limit has been exceeded. + """ + await recaptcha_box.verify_button.click() + + while recaptcha_box.frames_are_attached(): + if await recaptcha_box.rate_limit_is_visible(): + raise RecaptchaRateLimitError + + if ( + await recaptcha_box.challenge_is_solved() + or await recaptcha_box.try_again_is_visible() + ): + return + + if ( + await recaptcha_box.check_new_images_is_visible() + or await recaptcha_box.select_all_matching_is_visible() + ): + async with self._page.expect_response( + re.compile("/recaptcha/(api2|enterprise)/payload") + ) as response: + await recaptcha_box.new_challenge_button.click() + + await response.value + return await self._page.wait_for_timeout(250) @@ -450,7 +476,6 @@ async def _solve_image_challenge(self, recaptcha_box: AsyncRecaptchaBox) -> None If the reCAPTCHA rate limit has been exceeded. """ while recaptcha_box.frames_are_attached(): - await self._wait_for_value("_payload_response") await self._random_delay() capsolver_response = await self._get_capsolver_response( @@ -462,7 +487,13 @@ async def _solve_image_challenge(self, recaptcha_box: AsyncRecaptchaBox) -> None or not capsolver_response["solution"]["objects"] ): self._payload_response = None - await recaptcha_box.new_challenge_button.click() + + async with self._page.expect_response( + re.compile("/recaptcha/(api2|enterprise)/payload") + ) as response: + await recaptcha_box.new_challenge_button.click() + + await response.value continue await self._solve_tiles( @@ -475,25 +506,15 @@ async def _solve_image_challenge(self, recaptcha_box: AsyncRecaptchaBox) -> None button = recaptcha_box.skip_button.or_(recaptcha_box.next_button) if await button.is_visible(): - await button.click() - continue - - async with self._page.expect_response( - re.compile("/recaptcha/(api2|enterprise)/(payload|userverify)") - ): - await recaptcha_box.verify_button.click() - - if ( - await recaptcha_box.check_new_images_is_visible() - or await recaptcha_box.select_all_matching_is_visible() - ): + async with self._page.expect_response( + re.compile("/recaptcha/(api2|enterprise)/payload") + ) as response: await recaptcha_box.new_challenge_button.click() - else: - await self._wait_for_value("_token") - if await recaptcha_box.rate_limit_is_visible(): - raise RecaptchaRateLimitError + await response.value + continue + await self._submit_tile_answers(recaptcha_box) return async def _solve_audio_challenge(self, recaptcha_box: AsyncRecaptchaBox) -> None: @@ -521,11 +542,16 @@ async def _solve_audio_challenge(self, recaptcha_box: AsyncRecaptchaBox) -> None async with self._page.expect_response( re.compile("/recaptcha/(api2|enterprise)/payload") - ): + ) as response: await recaptcha_box.new_challenge_button.click() + await response.value + await self._submit_audio_text(recaptcha_box, text) + while self._token is None: + await self._page.wait_for_timeout(250) + def close(self) -> None: """Remove the response listener.""" try: diff --git a/playwright_recaptcha/recaptchav2/recaptcha_box.py b/playwright_recaptcha/recaptchav2/recaptcha_box.py index c3b18b6..332936a 100644 --- a/playwright_recaptcha/recaptchav2/recaptcha_box.py +++ b/playwright_recaptcha/recaptchav2/recaptcha_box.py @@ -344,8 +344,7 @@ def from_frames(cls, frames: Iterable[SyncFrame]) -> SyncRecaptchaBox: recaptcha_box = cls(anchor_frame, bframe_frame) if ( - recaptcha_box.checkbox.is_visible() - and not recaptcha_box.checkbox.is_checked() + not recaptcha_box.challenge_is_solved() or recaptcha_box.audio_challenge_button.is_visible() and recaptcha_box.audio_challenge_button.is_enabled() or recaptcha_box.image_challenge_button.is_visible() @@ -518,8 +517,7 @@ async def from_frames(cls, frames: Iterable[AsyncFrame]) -> AsyncRecaptchaBox: recaptcha_box = cls(anchor_frame, bframe_frame) if ( - await recaptcha_box.checkbox.is_visible() - and not await recaptcha_box.checkbox.is_checked() + not await recaptcha_box.challenge_is_solved() or await recaptcha_box.audio_challenge_button.is_visible() and await recaptcha_box.audio_challenge_button.is_enabled() or await recaptcha_box.image_challenge_button.is_visible() diff --git a/playwright_recaptcha/recaptchav2/sync_solver.py b/playwright_recaptcha/recaptchav2/sync_solver.py index 975699b..5d03f9c 100644 --- a/playwright_recaptcha/recaptchav2/sync_solver.py +++ b/playwright_recaptcha/recaptchav2/sync_solver.py @@ -139,18 +139,6 @@ def _random_delay(self, short: bool = True) -> None: delay_time = random.randint(150, 350) if short else random.randint(1250, 1500) self._page.wait_for_timeout(delay_time) - def _wait_for_value(self, attribute: str) -> None: - """ - Wait for an attribute to have a value. - - Parameters - ---------- - attribute : str - The attribute. - """ - while getattr(self, attribute) is None: - self._page.wait_for_timeout(250) - def _get_capsolver_response( self, recaptcha_box: SyncRecaptchaBox, image_data: bytes ) -> Optional[Dict[str, Any]]: @@ -307,7 +295,7 @@ def _click_checkbox(self, recaptcha_box: SyncRecaptchaBox) -> None: raise RecaptchaRateLimitError if recaptcha_box.challenge_is_visible(): - break + return self._page.wait_for_timeout(250) @@ -335,12 +323,10 @@ def _get_audio_url(self, recaptcha_box: SyncRecaptchaBox) -> str: raise RecaptchaRateLimitError if recaptcha_box.audio_challenge_is_visible(): - break + return recaptcha_box.audio_download_button.get_attribute("href") self._page.wait_for_timeout(250) - return recaptcha_box.audio_download_button.get_attribute("href") - def _submit_audio_text(self, recaptcha_box: SyncRecaptchaBox, text: str) -> None: """ Submit the reCAPTCHA audio text. @@ -364,8 +350,6 @@ def _submit_audio_text(self, recaptcha_box: SyncRecaptchaBox, text: str) -> None ): recaptcha_box.verify_button.click() - self._wait_for_value("_token") - while recaptcha_box.frames_are_attached(): if recaptcha_box.rate_limit_is_visible(): raise RecaptchaRateLimitError @@ -375,7 +359,46 @@ def _submit_audio_text(self, recaptcha_box: SyncRecaptchaBox, text: str) -> None or recaptcha_box.solve_failure_is_visible() or recaptcha_box.challenge_is_solved() ): - break + return + + self._page.wait_for_timeout(250) + + def _submit_tile_answers(self, recaptcha_box: SyncRecaptchaBox) -> None: + """ + Submit the reCAPTCHA image challenge tile answers. + + Parameters + ---------- + recaptcha_box : SyncRecaptchaBox + The reCAPTCHA box. + + Raises + ------ + RecaptchaRateLimitError + If the reCAPTCHA rate limit has been exceeded. + """ + recaptcha_box.verify_button.click() + + while recaptcha_box.frames_are_attached(): + if recaptcha_box.rate_limit_is_visible(): + raise RecaptchaRateLimitError + + if ( + recaptcha_box.challenge_is_solved() + or recaptcha_box.try_again_is_visible() + ): + return + + if ( + recaptcha_box.check_new_images_is_visible() + or recaptcha_box.select_all_matching_is_visible() + ): + with self._page.expect_response( + re.compile("/recaptcha/(api2|enterprise)/payload") + ): + recaptcha_box.new_challenge_button.click() + + return self._page.wait_for_timeout(250) @@ -396,7 +419,6 @@ def _solve_image_challenge(self, recaptcha_box: SyncRecaptchaBox) -> None: If the reCAPTCHA rate limit has been exceeded. """ while recaptcha_box.frames_are_attached(): - self._wait_for_value("_payload_response") self._random_delay() capsolver_response = self._get_capsolver_response( @@ -408,7 +430,12 @@ def _solve_image_challenge(self, recaptcha_box: SyncRecaptchaBox) -> None: or not capsolver_response["solution"]["objects"] ): self._payload_response = None - recaptcha_box.new_challenge_button.click() + + with self._page.expect_response( + re.compile("/recaptcha/(api2|enterprise)/payload") + ): + recaptcha_box.new_challenge_button.click() + continue self._solve_tiles(recaptcha_box, capsolver_response["solution"]["objects"]) @@ -418,25 +445,14 @@ def _solve_image_challenge(self, recaptcha_box: SyncRecaptchaBox) -> None: button = recaptcha_box.skip_button.or_(recaptcha_box.next_button) if button.is_visible(): - button.click() - continue - - with self._page.expect_response( - re.compile("/recaptcha/(api2|enterprise)/(payload|userverify)") - ): - recaptcha_box.verify_button.click() - - if ( - recaptcha_box.check_new_images_is_visible() - or recaptcha_box.select_all_matching_is_visible() + with self._page.expect_response( + re.compile("/recaptcha/(api2|enterprise)/payload") ): recaptcha_box.new_challenge_button.click() - else: - self._wait_for_value("_token") - if recaptcha_box.rate_limit_is_visible(): - raise RecaptchaRateLimitError + continue + self._submit_tile_answers(recaptcha_box) return def _solve_audio_challenge(self, recaptcha_box: SyncRecaptchaBox) -> None: @@ -469,6 +485,9 @@ def _solve_audio_challenge(self, recaptcha_box: SyncRecaptchaBox) -> None: self._submit_audio_text(recaptcha_box, text) + while self._token is None: + self._page.wait_for_timeout(250) + def close(self) -> None: """Remove the response listener.""" try: