├── .gitignore
├── LICENSE
├── README.md
├── defaults
└── startup_cookies.pkl
├── requirements.txt
├── sample_usage.py
├── utils
├── delay_config.py
├── exceptions.py
├── pprints.py
├── services.py
└── solver.py
└── yahoo_automator.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Abdul Moez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | YahooAutomator: Automated Yahoo Login Tool
2 | ====
3 | YahooAutomator, an automated tool designed for simplifying the Yahoo login process.
4 | This tool is specifically created for educational purposes to help individuals understand
5 | web automation and the challenges involved in automating login processes.
6 |
7 | Important Notice
8 | ----
9 | * **Educational Purpose Only**: YahooAutomator is intended for educational purposes, helping users learn about
10 | web automation and login processes.
11 | * **Responsible Usage**: Do not use this tool for malicious activities or unauthorized access.
12 | Respect the privacy and rights of others.
13 | * **Security**: If you discover any security vulnerabilities or issues related to this tool, please responsibly
14 | disclose them to the author.
15 |
16 | Author Information
17 | ----
18 | * **Author**: Abdul Moez
19 | * **Version**: 0.1
20 | * **Study**: Undergraduate at GCU Lahore, Pakistan
21 |
22 | Remember, unauthorized access to accounts or systems is illegal and unethical.
23 | Use YahooAutomator responsibly, following cybersecurity and privacy principles.
24 |
25 | -----------
26 | Video Demo
27 | -----------
28 | [](https://www.youtube.com/watch?v=x1DDWoVKnK0)
29 | -----------
30 |
31 |
32 | Introduction
33 | -----
34 | YahooAutomator provides functionality to automate the login to a
35 | Yahoo account using provided credentials, handle various challenges
36 | such as CAPTCHA, and interact with the Yahoo login page seamlessly.
37 | This tool aims to facilitate learning about web automation while emphasizing
38 | responsible and ethical usage.
39 |
40 | Features
41 | -----
42 | * **Automated Yahoo Login**: Effortlessly log in to your Yahoo account using the provided email and password.
43 | * **Challenges Handling**: Navigate through common challenges like CAPTCHA to ensure a smooth login process.
44 | * **Educational Purpose**: Designed for educational purposes, YahooAutomator serves as a valuable tool to
45 | understand web automation concepts.
46 |
47 | Getting Started
48 | -----
49 | To use YahooAutomator, follow these simple steps:
50 | 1. **Install FFmpeg**:
51 | * Windows
52 | ```shell
53 | winget install --id=Gyan.FFmpeg -e
54 | ```
55 | * Linux
56 | ```shell
57 | sudo apt install ffmpeg
58 | ```
59 | * MacOS
60 | ```shell
61 | brew install ffmpeg
62 | ```
63 | 2. Install Python Packages:
64 | ```shell
65 | pip3 install -r requirements.txt
66 | ```
67 | 3. Sample Usage:
68 | ```python
69 | from yahoo_automator import YahooAutomator
70 |
71 | yahoo_automator = YahooAutomator(
72 | profile_dir="./temp_profile",
73 | predefined_cookies="./defaults/startup_cookies.pkl"
74 | )
75 |
76 | status = yahoo_automator.login_to_yahoo(
77 | yahoo_mail="yahoo@yahoo.com",
78 | passwords_combo=["wrong_pass121", "wrong_pass212", "correctpassword"]
79 | )
80 |
81 | if status[0]:
82 | print(f"Successfully logged in with password: {yahoo_automator.correct_password}")
83 | else:
84 | print(f"Login failed. Reason: {status[1]}")
85 | ```
86 |
87 | YahooAutomator Parameters
88 | ----
89 | - `profile_dir (str)`: Path to the profile directory.
90 | - `predefined_cookies (str)`: Path to the file containing predefined cookies.
91 | - `estimated_wait (int)`: Estimated wait time for various actions.
92 | - `short_wait (int)`: Short wait time for specific actions.
93 | - `verbose (bool)`: Enable verbose mode for printing status messages.
94 | - `headless_mode (bool)`: Run the browser in headless mode.
95 |
96 | YahooAutomator Public Methods
97 | ----
98 | * `correct_password() -> str`: Returns the correct password used for a successful login.
99 | * `get_browser() -> WebDriver`: Get the WebDriver object for the current browser session.
100 | * `login_to_yahoo(yahoo_mail: str, passwords_combo: list[str], first_run: bool = True, pace: float = 0.5) -> tuple[bool, str]`: Logs in to a Yahoo account using provided credentials.
101 |
102 |
103 | Important tips
104 | -----
105 | * If you are using this tool to automate the emailing process, you cannot use any browser other than the one
106 | provided by this tool. You can create or utilize browsers created by the `login_to_yahoo` function
107 | automatically through the `get_browser()` function. It creates a detection-free
108 | browser for seamless integration with Yahoo.
109 | * Occasionally, you may face a **one-hour** ban from Yahoo, especially when attempting to log in for the
110 | first time without predefined cookies. In such cases, please wait for one hour before resuming your work.
111 |
112 | # Contributor
113 |
114 |
115 |
116 |
117 |
118 | -----------
119 | Support and Contact Information
120 | ----------
121 | > If you require any assistance or have questions, please feel free to reach out to me through the following channels:
122 | * **Email**: `abdulmoez123456789@gmail.com`
123 |
124 | > I have also established a dedicated Discord group for more interactive communication:
125 | * **Discord Server**: `https://discord.gg/RMNcqzmt9f`
126 |
127 |
128 | -----------
129 |
130 | Buy Me a coffee
131 | --------------
132 | __If you'd like to show your support and appreciation for my work, you can buy me a coffee using the
133 | following payment option:__
134 |
135 | **Payoneer**: `abdulmoez123456789@gmail.com`
136 |
137 | > Your support is greatly appreciated and helps me continue providing valuable assistance and resources.
138 | Thank you for your consideration.
139 |
140 |
--------------------------------------------------------------------------------
/defaults/startup_cookies.pkl:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Anonym0usWork1221/YahooAutomator/da55b5722adfe846f272136cebcebdcfd8df4d28/defaults/startup_cookies.pkl
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | async-generator==1.10
2 | attrs==22.2.0
3 | certifi==2023.7.22
4 | cffi==1.16.0
5 | charset-normalizer==3.0.1
6 | chromedriver-autoinstaller==0.6.3
7 | exceptiongroup==1.1.0
8 | h11==0.14.0
9 | idna==3.4
10 | iniconfig==2.0.0
11 | outcome==1.2.0
12 | packaging==23.2
13 | pluggy==1.0.0
14 | psutil==5.9.6
15 | pycparser==2.21
16 | pydub==0.25.1
17 | PySocks==1.7.1
18 | requests==2.31.0
19 | selenium==4.16.0
20 | sniffio==1.3.0
21 | sortedcontainers==2.4.0
22 | SpeechRecognition==3.8.1
23 | tomli==2.0.1
24 | trio==0.22.0
25 | trio-websocket==0.9.2
26 | undetected-chromedriver==3.5.4
27 | urllib3==1.26.14
28 | websockets==12.0
29 | wsproto==1.2.0
30 |
--------------------------------------------------------------------------------
/sample_usage.py:
--------------------------------------------------------------------------------
1 | from yahoo_automator import YahooAutomator
2 | # winget install --id=Gyan.FFmpeg -e
3 | # sudo apt install ffmpeg
4 |
5 | yahoo_automator = YahooAutomator(profile_dir="./temp_profile",
6 | predefined_cookies="./defaults/startup_cookies.pkl"
7 | )
8 |
9 | status = yahoo_automator.login_to_yahoo(
10 | yahoo_mail="yahoo@yahoo.com",
11 | passwords_combo=["wrong_pass121", "wrong_pass212", "correctpassword"])
12 |
13 | if status[0]:
14 | print(f"Found password: {yahoo_automator.correct_password}")
15 | else:
16 | print(status)
17 | print(yahoo_automator.correct_password)
18 |
--------------------------------------------------------------------------------
/utils/delay_config.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | import random
3 | import time
4 |
5 |
6 | class DelayConfig(ABC):
7 | """Abstract base class for delay configurations for :class:`RecaptchaSolver`."""
8 |
9 | @abstractmethod
10 | def delay_after_click_checkbox(self):
11 | pass
12 |
13 | @abstractmethod
14 | def delay_after_click_audio_button(self):
15 | pass
16 |
17 | @abstractmethod
18 | def delay_after_click_verify_button(self):
19 | pass
20 |
21 |
22 | class StandardDelayConfig(DelayConfig):
23 | def __init__(self, min_delay: float = 0.75, max_delay: float = 1.25) -> None:
24 | self.min_delay = min_delay
25 | self.max_delay = max_delay
26 |
27 | def _sleep_random(self) -> None:
28 | time.sleep(random.uniform(self.min_delay, self.max_delay))
29 |
30 | def delay_after_click_checkbox(self):
31 | self._sleep_random()
32 |
33 | def delay_after_click_audio_button(self):
34 | self._sleep_random()
35 |
36 | def delay_after_click_verify_button(self):
37 | self._sleep_random()
38 |
--------------------------------------------------------------------------------
/utils/exceptions.py:
--------------------------------------------------------------------------------
1 | class RecaptchaException(Exception):
2 | pass
3 |
--------------------------------------------------------------------------------
/utils/pprints.py:
--------------------------------------------------------------------------------
1 | """
2 | PPrints Module
3 | This module defines the PPrints class, which provides utilities for printing formatted information and handling logs.
4 | Classes:
5 | PPrints: A utility class for pretty printing information and handling logs.
6 | """
7 |
8 | # Import necessary modules
9 |
10 | from platform import system as system_platform
11 | from threading import active_count
12 | from os.path import isfile
13 | from psutil import Process
14 | from os import system
15 |
16 |
17 | class PPrints:
18 | """
19 | PPrints Class
20 | This class encapsulates methods for printing formatted information and optionally logging it.
21 | Attributes:
22 | _HEADER (str): ANSI escape sequence for header color.
23 | _BLUE (str): ANSI escape sequence for blue color.
24 | _CYAN (str): ANSI escape sequence for cyan color.
25 | _GREEN (str): ANSI escape sequence for green color.
26 | _WARNING (str): ANSI escape sequence for warning color.
27 | _RED (str): ANSI escape sequence for red color.
28 | _RESET (str): ANSI escape sequence to reset text color.
29 | Methods:
30 | __init__(self, logs_file="logs.txt"): Initialize the PPrints object.
31 | clean_terminal(): Clean the terminal screen based on the platform.
32 | pretty_print(current_site, status, mode, limit, total_results="Calculating",
33 | current_index="Calculating", logs=False): Print formatted information with optional logging.
34 | """
35 |
36 | # Private elements
37 | _HEADER = '\033[95m'
38 | _BLUE = '\033[94m'
39 | _CYAN = '\033[96m'
40 | _GREEN = '\033[92m'
41 | _WARNING = '\033[93m'
42 | _RED = '\033[91m'
43 | _RESET = '\033[0m'
44 |
45 | def __init__(self, logs_file: str = "logs.txt") -> None:
46 | """
47 | Initialize the PPrints object.
48 | Args:
49 | logs_file (str): The name of the log file to write logs to.
50 | """
51 |
52 | self._process = Process()
53 | self._log_file = logs_file
54 |
55 | @staticmethod
56 | def clean_terminal() -> str:
57 | """
58 | Clean the terminal screen based on the platform.
59 | Returns:
60 | str: The name of the platform.
61 | """
62 |
63 | if system_platform().lower() == "windows":
64 | system("cls")
65 | else:
66 | system("clear")
67 | return system_platform()
68 |
69 | def pretty_print(self,
70 | status: str,
71 | mode: str,
72 | logs: bool = False,
73 | ) -> None:
74 | """
75 | Print formatted information along with optional logging.
76 | Args:
77 | status (str): The status of the process.
78 | mode (str): The mode of operation.
79 | logs (bool, optional): Whether to log the information. Defaults to False.
80 | """
81 |
82 | memory_info = self._process.memory_info()
83 | current_memory_usage = memory_info.rss / 1024 / 1024 # Convert bytes to megabytes
84 |
85 | print(f"{self._GREEN}Platform: {self.clean_terminal()}\n"
86 | f"{self._WARNING}Status: {status}\n"
87 | f"{self._BLUE}Mode: {mode}\n"
88 | f"{self._BLUE}LaunchedDrivers: {active_count()}\n"
89 | f"{self._RED}MemoryUsageByScript: {current_memory_usage: .2f}MB\n{self._RESET}")
90 | if logs:
91 | log_msg = f"Status: {status}\n" \
92 | f"Mode: {mode}\n\n"
93 | if isfile(self._log_file):
94 | with open(self._log_file, 'a') as file_obj:
95 | file_obj.write(log_msg)
96 | else:
97 | with open(self._log_file, 'w') as file_obj:
98 | file_obj.write(log_msg)
99 |
--------------------------------------------------------------------------------
/utils/services.py:
--------------------------------------------------------------------------------
1 | from speech_recognition import AudioData
2 | from abc import ABC, abstractmethod
3 | from typing import Any, Optional
4 | import speech_recognition as sr
5 |
6 |
7 | class Service(ABC):
8 | """Abstract base class for speech recognition services."""
9 |
10 | @abstractmethod
11 | def __init__(self) -> None:
12 | pass
13 |
14 | @abstractmethod
15 | def recognize(self, recognizer: sr.Recognizer, audio_data: AudioData, language: str = 'en-US') -> Any:
16 | """Perform speech recognition on the given audio data using the given recognizer."""
17 | pass
18 |
19 |
20 | class GoogleService(Service):
21 | """
22 | Service for Google Speech Recognition API.
23 |
24 | See docs for `speech_recognition.Recognizer.recognize_google` for details on congfiguration.
25 | """
26 |
27 | def __init__(
28 | self,
29 | key: Optional[Any] = None,
30 | ) -> None:
31 | self.key = key
32 |
33 | def recognize(self, recognizer: sr.Recognizer, audio_data: AudioData, language: str = 'en-US') -> Any:
34 | return recognizer.recognize_google(audio_data, key=self.key, language=language)
35 |
36 |
37 | class GoogleCloudService(Service):
38 | """
39 | Service for Google Cloud Speech API.
40 |
41 | See docs for `speech_recognition.Recognizer.recognize_google_cloud` for details on congfiguration.
42 | """
43 |
44 | def __init__(
45 | self,
46 | credentials_json: Optional[Any] = None,
47 | ) -> None:
48 | self.credentials_json = credentials_json
49 |
50 | def recognize(self, recognizer: sr.Recognizer, audio_data: AudioData, language: str = 'en-US') -> Any:
51 | return recognizer.recognize_google_cloud(
52 | audio_data, credentials_json=self.credentials_json
53 | )
54 |
--------------------------------------------------------------------------------
/utils/solver.py:
--------------------------------------------------------------------------------
1 | from selenium.webdriver.support import expected_conditions as ec
2 | from selenium.webdriver.remote.webelement import WebElement
3 | from selenium.webdriver.chrome.webdriver import WebDriver
4 | from selenium.webdriver.support.ui import WebDriverWait
5 | from selenium.common.exceptions import TimeoutException
6 | from utils.services import Service, GoogleService
7 | from utils.exceptions import RecaptchaException
8 | from selenium.webdriver.common.by import By
9 | from utils.delay_config import DelayConfig
10 | import speech_recognition as sr
11 | from pydub import AudioSegment
12 | from typing import Optional
13 | import tempfile
14 | import requests
15 | import random
16 | import uuid
17 | import time
18 | import os
19 |
20 | DEFAULT_SERVICE: Service = GoogleService()
21 |
22 |
23 | class RecaptchaSolver:
24 | def __init__(
25 | self,
26 | driver: WebDriver,
27 | service: Service = DEFAULT_SERVICE,
28 | service_language: str = 'en-US',
29 | delay_config: Optional[DelayConfig] = None,
30 | ):
31 | """
32 | :param driver: Selenium web driver to use to solve the captcha
33 | :param service: service to use for speech recognition (defaults to ``GoogleService``).
34 | See the ``services`` module for available services.
35 | :param service_language: Language to use when recognizing speech to solve reCAPTCHA challenge (en-US by default for American English recognition)
36 | :param delay_config: if set, use the given configuration for delays between UI interactions.
37 | See :class:`DelayConfig`, and also :class:`StandardDelayConfig`, which provides a standard implementation that should work in many cases.
38 | """
39 |
40 | self._driver = driver
41 | self._service = service
42 | self._delay_config = delay_config
43 | self._language = service_language
44 |
45 | # Initialise speech recognition API object
46 | self._recognizer = sr.Recognizer()
47 |
48 | def click_recaptcha_v2(self, iframe: WebElement, by_selector: Optional[str] = None) -> None:
49 | """
50 | Click the "I'm not a robot" checkbox and then solve a reCAPTCHA v2 challenge.
51 |
52 | Call this method directly on web pages with an "I'm not a robot" checkbox. See for details of how this works.
53 |
54 | :param iframe: web element for inline frame of reCAPTCHA to solve
55 | :param by_selector: By selector to use to find the iframe, if ``iframe`` is a string
56 | :raises selenium.common.exceptions.TimeoutException: if a timeout occurred while waiting
57 | """
58 |
59 | if isinstance(iframe, str):
60 | WebDriverWait(self._driver, 10).until(
61 | ec.frame_to_be_available_and_switch_to_it((by_selector, iframe)))
62 |
63 | else:
64 | self._driver.switch_to.frame(iframe)
65 |
66 | checkbox = self._wait_for_element(
67 | by='id',
68 | locator='recaptcha-anchor',
69 | timeout=10,
70 | )
71 |
72 | self._js_click(checkbox)
73 |
74 | if checkbox.get_attribute('aria-checked') == 'true':
75 | return
76 |
77 | if self._delay_config:
78 | self._delay_config.delay_after_click_checkbox()
79 |
80 | self._driver.switch_to.parent_frame()
81 |
82 | captcha_challenge = self._wait_for_element(
83 | by=By.XPATH,
84 | locator='//iframe[contains(@src, "recaptcha") and contains(@src, "bframe")]',
85 | timeout=5,
86 | )
87 |
88 | self.solve_recaptcha_v2_challenge(iframe=captcha_challenge)
89 |
90 | def solve_recaptcha_v2_challenge(self, iframe: WebElement) -> None:
91 | """
92 | Solve a reCAPTCHA v2 challenge that has already appeared.
93 |
94 | Call this method directly on web pages with the "invisible reCAPTCHA" badge. See for details of how this works.
95 |
96 | :param iframe: Web element for inline frame of reCAPTCHA to solve
97 | :raises selenium.common.exceptions.TimeoutException: if a timeout occurred while waiting
98 | """
99 |
100 | self._driver.switch_to.frame(iframe)
101 |
102 | # If the captcha image audio is available, locate it. Otherwise, skip to the next line of code.
103 |
104 | try:
105 | self._wait_for_element(
106 | by=By.XPATH,
107 | locator='//*[@id="recaptcha-audio-button"]',
108 | timeout=1,
109 | ).click()
110 |
111 | except TimeoutException:
112 | pass
113 |
114 | self._solve_audio_challenge(self._language)
115 |
116 | # Locate verify button and click it via JavaScript
117 | verify_button = self._wait_for_element(
118 | by=By.ID,
119 | locator='recaptcha-verify-button',
120 | timeout=5,
121 | )
122 |
123 | self._js_click(verify_button)
124 |
125 | if self._delay_config:
126 | self._delay_config.delay_after_click_verify_button()
127 |
128 | try:
129 | self._wait_for_element(
130 | by=By.XPATH,
131 | locator='//div[normalize-space()="Multiple correct solutions required - please solve more."]',
132 | timeout=1,
133 | )
134 |
135 | self._solve_audio_challenge(self._language)
136 |
137 | # Locate verify button again to avoid stale element reference and click it via JavaScript
138 | second_verify_button = self._wait_for_element(
139 | by=By.ID,
140 | locator='recaptcha-verify-button',
141 | timeout=5,
142 | )
143 |
144 | self._js_click(second_verify_button)
145 |
146 | except TimeoutException:
147 | pass
148 |
149 | self._driver.switch_to.parent_frame()
150 |
151 | def _solve_audio_challenge(self, language: str) -> None:
152 | try:
153 | # Locate audio challenge download link
154 | download_link: WebElement = self._wait_for_element(
155 | by=By.CLASS_NAME,
156 | locator='rc-audiochallenge-tdownload-link',
157 | timeout=10,
158 | )
159 |
160 | except TimeoutException:
161 | raise RecaptchaException('Google has detected automated queries. Try again later.')
162 |
163 | # Create temporary directory and temporary files
164 | tmp_dir = tempfile.gettempdir()
165 |
166 | id_ = uuid.uuid4().hex
167 |
168 | mp3_file, wav_file = os.path.join(tmp_dir, f'{id_}_tmp.mp3'), os.path.join(tmp_dir, f'{id_}_tmp.wav')
169 |
170 | tmp_files = {mp3_file, wav_file}
171 |
172 | with open(mp3_file, 'wb') as f:
173 | link = download_link.get_attribute('href')
174 |
175 | audio_download = requests.get(url=link, allow_redirects=True)
176 |
177 | f.write(audio_download.content)
178 |
179 | f.close()
180 |
181 | # Convert MP3 to WAV format for compatibility with speech recognizer APIs
182 | AudioSegment.from_mp3(mp3_file).export(wav_file, format='wav')
183 |
184 | # Disable dynamic energy threshold to avoid failed reCAPTCHA audio transcription due to static noise
185 | self._recognizer.dynamic_energy_threshold = False
186 |
187 | with sr.AudioFile(wav_file) as source:
188 | audio = self._recognizer.listen(source)
189 |
190 | try:
191 | recognized_text = self._service.recognize(self._recognizer, audio, language)
192 |
193 | except sr.UnknownValueError:
194 | raise RecaptchaException('Speech recognition API could not understand audio, try again')
195 |
196 | # Clean up all temporary files
197 | for path in tmp_files:
198 | if os.path.exists(path):
199 | os.remove(path)
200 |
201 | # Write transcribed text to iframe's input box
202 | response_textbox = self._driver.find_element(By.ID, 'audio-response')
203 |
204 | self._human_type(element=response_textbox, text=recognized_text)
205 |
206 | def _js_click(self, element: WebElement) -> None:
207 | """
208 | Perform click on given web element using JavaScript.
209 |
210 | :param element: web element to click
211 | """
212 |
213 | self._driver.execute_script('arguments[0].click();', element)
214 |
215 | def _wait_for_element(
216 | self,
217 | by: str = By.ID,
218 | locator: Optional[str] = None,
219 | timeout: float = 10,
220 | ) -> WebElement:
221 | """
222 | Try to locate web element within given duration.
223 |
224 | :param by: strategy to use to locate element (see class `selenium.webdriver.common.by.By`)
225 | :param locator: locator that identifies the element
226 | :param timeout: number of seconds to wait for element before raising `TimeoutError`
227 | :return: located web element
228 | :raises selenium.common.exceptions.TimeoutException: if element is not located within given duration
229 | """
230 |
231 | return WebDriverWait(self._driver, timeout).until(ec.visibility_of_element_located((by, locator)))
232 |
233 | @staticmethod
234 | def _human_type(element: WebElement, text: str) -> None:
235 | """
236 | Types in a way reminiscent of a human, with a random delay in between 50ms to 100ms for every character
237 | :param element: Input element to type text to
238 | :param text: Input to be typed
239 | """
240 |
241 | for c in text:
242 | element.send_keys(c)
243 |
244 | time.sleep(random.uniform(0.05, 0.1))
245 |
246 |
247 | # Add alias for backwards compatibility
248 | API = RecaptchaSolver
249 |
--------------------------------------------------------------------------------
/yahoo_automator.py:
--------------------------------------------------------------------------------
1 | """
2 | *******************************************************************************
3 | WARNING: USE THIS TOOL RESPONSIBLY
4 | *******************************************************************************
5 |
6 | This YahooAutomator tool, developed by Abdul Moez, is intended for educational purposes only and should be used
7 | responsibly and ethically. The tool automates the Yahoo login process, providing functionality to log in to a
8 | Yahoo account using provided credentials, handle various challenges like CAPTCHA,
9 | and interact with the Yahoo login page.
10 |
11 | By using this tool, you acknowledge and agree to the following:
12 |
13 | 1. Educational Purpose Only:
14 | This tool is meant for educational purposes, helping individuals understand web automation and
15 | the challenges involved in automating login processes.
16 |
17 | 2. Not for Malicious Activities:
18 | Do not use this tool for any malicious or harmful activities, including but not limited to unauthorized
19 | access, data theft, or any actions that violate the law or Yahoo's terms of service.
20 |
21 | 3. Responsibility:
22 | The author, Abdul Moez, is not responsible for any misuse, damage, or legal consequences resulting from
23 | the use of this tool. Users are solely responsible for their actions.
24 |
25 | 4. User Accountability:
26 | Users of this tool should exercise caution, adhere to legal and ethical standards, and respect the
27 | privacy and rights of others. Any consequences arising from misuse will be the user's responsibility.
28 |
29 | 5. No Warranty:
30 | This tool comes with no warranty. The author provides no guarantees regarding its performance,
31 | accuracy, or suitability for any specific purpose.
32 |
33 | 6. Report Security Issues:
34 | If you discover any security vulnerabilities or issues related to this tool,
35 | please responsibly disclose them to the author.
36 |
37 | 7. Author information:
38 | - author: Abdul Moez
39 | - version: 0.1
40 | - study: Undergraduate at GCU Lahore, Pakistan
41 |
42 | Remember that unauthorized access to accounts or systems is illegal and unethical. Use this tool responsibly,
43 | respecting the principles of cybersecurity and privacy.
44 |
45 | *******************************************************************************
46 | """
47 |
48 | from selenium.common.exceptions import NoSuchElementException, TimeoutException
49 | from selenium.webdriver.common.action_chains import ActionChains
50 | from undetected_chromedriver import Chrome, ChromeOptions
51 | from selenium.webdriver.chrome.webdriver import WebDriver
52 | from utils.exceptions import RecaptchaException
53 | from selenium.webdriver.common.by import By
54 | from utils.solver import RecaptchaSolver
55 | from utils.pprints import PPrints
56 | import chromedriver_autoinstaller
57 | from os import path, makedirs
58 | from pickle import load
59 | from time import sleep
60 | from re import search
61 |
62 |
63 | class YahooAutomator(object):
64 | """
65 | YahooAutomator is a class designed for automating the Yahoo login process. It provides functionality to log in
66 | to a Yahoo account using provided credentials, handle various challenges like CAPTCHA, and interact with the
67 | Yahoo login page.
68 |
69 | Attributes:
70 | __TARGET_SITE (str): The target URL for Yahoo login.
71 | __class_new_account (str): Selector for the 'Create New Account' link.
72 | __class_password_error (str): Selector for the element displaying password-related errors.
73 | __class_unexpected_error (str): Selector for the element displaying unexpected errors.
74 | __id_username (str): Selector for the username input field.
75 | __id_password (str): Selector for the password input field.
76 | __id_banned_time (str): Selector for the element displaying the time remaining for a first-time ban.
77 | __id_captcha_submit_btn (str): Selector for the reCAPTCHA submit button.
78 | __id_password_signin (str): Selector for the 'Sign In' button after entering the password.
79 | __name_email_signin (str): Selector for the 'Sign In' button after entering the email.
80 | __css_recaptcha_iframe_1 (str): CSS selector for the first reCAPTCHA iframe.
81 | __xpath_recaptcha_iframe_2 (str): XPath selector for the second reCAPTCHA iframe.
82 | __banned_time_exp (str): Regular expression for extracting remaining time from a ban message.
83 |
84 | Methods:
85 | __init__(self, profile_dir: str, predefined_cookies: str, estimated_wait: int = 10, short_wait: int = 5,
86 | verbose: bool = True, headless_mode: bool = False) -> None:
87 | Initializes the YahooAutomator object.
88 |
89 | correct_password(self) -> str:
90 | Returns the correct password used for a successful login.
91 |
92 | __pprints_override(self, status: str, logs: bool = False) -> None:
93 | Overrides and prints status messages using the PPrints class.
94 |
95 | __created_patched_driver(self) -> WebDriver:
96 | Creates and returns a patched WebDriver for interacting with the browser.
97 |
98 | get_browser(self) -> WebDriver:
99 | Get the WebDriver object for the current browser session.
100 |
101 | __validate_directories(self) -> None:
102 | Validates the existence of the profile directory and creates it if necessary.
103 |
104 | __skip_old_login_page(self) -> None:
105 | Skip the old Yahoo login page if present.
106 |
107 | __insert_email(self, email: str, pace: float = .5) -> bool:
108 | Inserts the Yahoo email address into the login form.
109 |
110 | __check_yahoo_first_time_ban(self) -> tuple[bool, str]:
111 | Checks if the Yahoo account is first-time banned.
112 |
113 | __solve_captcha(self) -> tuple[bool, str]:
114 | Solves the reCAPTCHA challenge if present.
115 |
116 | __insert_password(self, password: str, pace: float = 0.5) -> bool:
117 | Inserts the password into the login form.
118 |
119 | __is_password_correct(self) -> bool:
120 | Checks if the last entered password is correct.
121 |
122 | __is_unexpected_error(self) -> tuple[bool, str]:
123 | Checks if there is an unexpected error during the login process.
124 |
125 | login_to_yahoo(self, yahoo_mail: str, passwords_combo: list[str], first_run: bool = True,
126 | pace: float = 0.5) -> tuple[bool, str]:
127 | Logs in to a Yahoo account using provided credentials.
128 | """
129 |
130 | __TARGET_SITE: str = "https://login.yahoo.com/"
131 |
132 | # selectors
133 | __class_new_account: str = "bottom-cta"
134 | __class_password_error: str = "error-msg"
135 | __class_unexpected_error: str = "notification-error"
136 | __id_username: str = "login-username"
137 | __id_password: str = "login-passwd"
138 | __id_banned_time: str = "wait-challenge"
139 | __id_captcha_submit_btn: str = "recaptcha-submit"
140 | __id_password_signin: str = "login-signin"
141 | __name_email_signin: str = "signin"
142 | __css_recaptcha_iframe_1: str = 'iframe[id="recaptcha-iframe"]'
143 | __xpath_recaptcha_iframe_2: str = '//iframe[@title="reCAPTCHA"]'
144 |
145 | # regular expressions
146 | __banned_time_exp: str = r"Try again in (\d+) minutes"
147 |
148 | def __init__(self, profile_dir: str, predefined_cookies: str, estimated_wait: int = 10, short_wait: int = 5,
149 | verbose: bool = True, headless_mode: bool = False) -> None:
150 | """
151 | Initialize the YahooAutomator object.
152 |
153 | Parameters:
154 | - profile_dir (str): Path to the profile directory.
155 | - predefined_cookies (str): Path to the file containing predefined cookies.
156 | - estimated_wait (int): Estimated wait time for various actions.
157 | - short_wait (int): Short wait time for specific actions.
158 | - verbose (bool): Enable verbose mode for printing status messages.
159 | - headless_mode (bool): Run the browser in headless mode.
160 |
161 | Returns:
162 | None
163 | """
164 |
165 | self.__mode: str = "Headless" if headless_mode else "Windowed"
166 | self.__current_driver: any([None, WebDriver]) = None
167 | self.__predefined_cookies: str = predefined_cookies
168 | self.__estimated_wait: int = estimated_wait
169 | self.__headless: bool = headless_mode
170 | self.__profile_dir: str = profile_dir
171 | self.__short_wait: int = short_wait
172 | self.__pprints: PPrints = PPrints()
173 | self.__verbose: bool = verbose
174 | self.__validate_directories()
175 | self.__founded_password: str = ""
176 |
177 | def __validate_directories(self):
178 | """
179 | Validate the existence of the profile directory and create it if necessary.
180 | Returns:
181 | None
182 | """
183 |
184 | profile_dir: str = path.abspath(self.__profile_dir)
185 | makedirs(name=profile_dir, exist_ok=True)
186 |
187 | @property
188 | def correct_password(self) -> str:
189 | """
190 | Get the correct password used for successful login.
191 |
192 | Returns:
193 | str: Correct password.
194 | """
195 |
196 | return self.__founded_password
197 |
198 | def __pprints_override(self, status: str, logs: bool = False) -> None:
199 | """
200 | Override and print status messages using PPrints class.
201 |
202 | Parameters:
203 | - status (str): Status message to be printed.
204 | - logs (bool): Flag indicating whether to print logs.
205 |
206 | Returns:
207 | None
208 | """
209 |
210 | if self.__verbose:
211 | self.__pprints.pretty_print(status=status, mode=self.__mode, logs=logs)
212 |
213 | def __created_patched_driver(self) -> WebDriver:
214 | """
215 | Create and return a patched WebDriver for interacting with the browser.
216 | Returns:
217 | WebDriver: The patched WebDriver object.
218 | """
219 |
220 | self.__pprints_override(status="Initializing patched browser")
221 | version_main = int(chromedriver_autoinstaller.get_chrome_version().split(".")[0])
222 | profile_dir: str = path.abspath(self.__profile_dir)
223 | options = ChromeOptions()
224 | options.add_argument('--allow-running-insecure-content')
225 | options.add_argument(f'--user-data-dir={profile_dir}')
226 | options.add_argument('--ignore-certificate-errors')
227 | options.add_argument("--disable-notifications")
228 | options.add_argument("--window-size=1920,1080")
229 | options.add_argument("--disable-infobars")
230 | if self.__headless:
231 | driver = Chrome(options=options, enable_console_log=False, enable_logging=False,
232 | version_main=version_main, headless=True)
233 | else:
234 | driver = Chrome(options=options, enable_console_log=False, enable_logging=False,
235 | version_main=version_main)
236 | return driver
237 |
238 | def get_browser(self) -> WebDriver:
239 | """
240 | Get the WebDriver object for the current browser session.
241 | Returns:
242 | WebDriver: The WebDriver object.
243 | """
244 |
245 | if not self.__current_driver:
246 | self.__current_driver = self.__created_patched_driver()
247 | return self.__current_driver
248 |
249 | def __skip_old_login_page(self) -> None:
250 | """
251 | Skip the old Yahoo login page if present.
252 | Returns:
253 | None
254 | """
255 |
256 | try:
257 | self.__current_driver.find_element(By.CLASS_NAME, self.__class_new_account).click()
258 | except NoSuchElementException:
259 | ...
260 |
261 | def __insert_email(self, email: str, pace: float = .5) -> bool:
262 | """
263 | Insert the Yahoo email address into the login form.
264 |
265 | Parameters:
266 | - email (str): Yahoo email address.
267 | - pace (float): Typing pace for entering characters.
268 |
269 | Returns:
270 | bool: True if successful, False otherwise.
271 | """
272 |
273 | self.__pprints_override(status=f"Performing email address operation on: {email}")
274 | actions: ActionChains = ActionChains(driver=self.__current_driver)
275 | try:
276 | email_block = self.__current_driver.find_element(by=By.ID, value=self.__id_username)
277 | email_block.click()
278 | except NoSuchElementException:
279 | return False
280 | for char in email:
281 | actions.send_keys(char)
282 | actions.perform()
283 | sleep(pace)
284 | sleep(.4)
285 | try:
286 | self.__current_driver.find_element(by=By.NAME, value=self.__name_email_signin).click()
287 | except NoSuchElementException:
288 | return False
289 | sleep(self.__short_wait)
290 | return True
291 |
292 | def __check_yahoo_first_time_ban(self) -> tuple[bool, str]:
293 | """
294 | Check if the Yahoo account is first-time banned.
295 |
296 | Returns:
297 | tuple[bool, str]: Tuple indicating ban status and status message.
298 | """
299 |
300 | self.__pprints_override(status="Checking yahoo ban traces")
301 | try:
302 | banned_timer_text: str = self.__current_driver.find_element(by=By.ID, value=self.__id_banned_time).text
303 | ban_remaining_time: any([None, str]) = search(pattern=self.__banned_time_exp, string=banned_timer_text)
304 | if ban_remaining_time:
305 | minutes = int(ban_remaining_time.group(1))
306 | error_text: str = f"Your ip has been baned and unblock after: {minutes} minutes."
307 | self.__pprints_override(status=error_text)
308 | return False, error_text
309 | else:
310 | return False, banned_timer_text
311 | except NoSuchElementException:
312 | return True, ""
313 |
314 | def __solve_captcha(self) -> tuple[bool, str]:
315 | """
316 | Solve the reCAPTCHA challenge if present.
317 |
318 | Returns:
319 | tuple[bool, str]: Tuple indicating a success status and status message.
320 | """
321 |
322 | self.__pprints_override(status="Checking for captcha")
323 | try:
324 | captcha_block_iframe = self.__current_driver.find_element(by=By.CSS_SELECTOR,
325 | value=self.__css_recaptcha_iframe_1)
326 | self.__current_driver.switch_to.frame(frame_reference=captcha_block_iframe)
327 | recaptcha_iframe = self.__current_driver.find_element(by=By.XPATH, value=self.__xpath_recaptcha_iframe_2)
328 | captcha_solver = RecaptchaSolver(driver=self.__current_driver)
329 | try:
330 | captcha_solver.click_recaptcha_v2(iframe=recaptcha_iframe)
331 | except TimeoutException:
332 | return True, ""
333 | except RecaptchaException:
334 | self.__current_driver.switch_to.default_content()
335 | return False, "Recaptcha solver is detected"
336 | self.__current_driver.switch_to.default_content()
337 | try:
338 | self.__current_driver.find_element(by=By.ID, value=self.__id_captcha_submit_btn).click()
339 | except NoSuchElementException:
340 | return False, "Does not able to find captcha submit button"
341 | return True, ""
342 | except NoSuchElementException:
343 | return True, ""
344 |
345 | def __insert_password(self, password: str, pace: float = 0.5) -> bool:
346 | """
347 | Insert the password into the login form.
348 |
349 | Parameters:
350 | - password (str): Password to be entered.
351 | - pace (float): Typing pace for entering characters.
352 |
353 | Returns:
354 | bool: True if successful, False otherwise.
355 | """
356 |
357 | self.__pprints_override(status=f"Performing password address operation on: {password}")
358 | actions: ActionChains = ActionChains(driver=self.__current_driver)
359 | try:
360 | password_block = self.__current_driver.find_element(by=By.ID, value=self.__id_password)
361 | password_block.click()
362 | except NoSuchElementException:
363 | return False
364 | for char in password:
365 | actions.send_keys(char)
366 | actions.perform()
367 | sleep(pace)
368 | sleep(.4)
369 | try:
370 | self.__current_driver.find_element(by=By.ID, value=self.__id_password_signin).click()
371 | except NoSuchElementException:
372 | return False
373 | sleep(self.__short_wait - 2 if self.__short_wait - 3 > 0 else 2)
374 | return True
375 |
376 | def __is_password_correct(self) -> bool:
377 | """
378 | Check if the last-entered password is correct.
379 |
380 | Returns:
381 | bool: True if correct, False otherwise.
382 | """
383 |
384 | self.__pprints_override(status="Checking if last password is correct")
385 | try:
386 | error_message: str = self.__current_driver.find_element(by=By.CLASS_NAME,
387 | value=self.__class_password_error).text.strip()
388 | if "invalid password" in error_message.lower():
389 | return False
390 | return True
391 | except NoSuchElementException:
392 | return True
393 |
394 | def __is_unexpected_error(self) -> tuple[bool, str]:
395 | """
396 | Check if there is an unexpected error during the login process.
397 |
398 | Returns:
399 | tuple[bool, str]: Tuple indicating error status and error message.
400 | """
401 |
402 | try:
403 | error_notification: str = self.__current_driver.find_element(
404 | by=By.CLASS_NAME, value=self.__class_unexpected_error
405 | ).text.strip()
406 | return True, f"Unsolvable error: {error_notification}"
407 | except NoSuchElementException:
408 | return False, ""
409 |
410 | def login_to_yahoo(self, yahoo_mail: str, passwords_combo: list[str], first_run: bool = True,
411 | pace: float = 0.5) -> tuple[bool, str]:
412 | """
413 | Log in to a Yahoo account using provided credentials.
414 | Parameters:
415 | - yahoo_mail (str): Yahoo email address.
416 | - passwords_combo (list[str]): List of passwords to try.
417 | - first_run (bool): Flag indicating if it's the first run.
418 | - pace (float): Typing pace for email and password.
419 | Returns:
420 | tuple[bool, str]: Tuple indicating a success status and status message.
421 | """
422 |
423 | if not self.__current_driver:
424 | self.__current_driver = self.__created_patched_driver()
425 |
426 | self.__current_driver.get(url=self.__TARGET_SITE)
427 | if first_run:
428 | self.__pprints_override(status="Loading predefined cookies")
429 | pre_defined_cookies = load(file=open(self.__predefined_cookies, 'rb'))
430 | for cookie in pre_defined_cookies:
431 | self.__current_driver.add_cookie(cookie)
432 | self.__current_driver.refresh()
433 | self.__skip_old_login_page()
434 | sleep(self.__estimated_wait)
435 | if not self.__insert_email(email=yahoo_mail, pace=pace):
436 | return False, "An error occurred while performing the email entering function."
437 |
438 | ban_status: tuple[bool, str] = self.__check_yahoo_first_time_ban()
439 | if not ban_status[0]:
440 | return ban_status
441 | captcha_checks: tuple[bool, str] = self.__solve_captcha()
442 | if not captcha_checks[0]:
443 | return captcha_checks
444 |
445 | for password in passwords_combo:
446 | sleep(self.__short_wait - 3 if self.__short_wait - 3 > 0 else 2)
447 | if not self.__insert_password(password=password, pace=pace):
448 | return False, "An error occurred while performing the password entering function."
449 | if self.__is_password_correct():
450 | self.__pprints_override(status=f"Password matched; account is logged in using: {password}")
451 | self.__founded_password = password
452 | return True, f"Password matched; account is logged in using: {password}"
453 | unexpected_error: tuple[bool, str] = self.__is_unexpected_error()
454 | if unexpected_error[0]:
455 | self.__pprints_override(status=unexpected_error[1])
456 | return False, unexpected_error[1]
457 | captcha_checks: tuple[bool, str] = self.__solve_captcha()
458 | if not captcha_checks[0]:
459 | return captcha_checks
460 |
--------------------------------------------------------------------------------