├── wplay ├── __init__.py ├── utils │ ├── __init__.py │ ├── verify_internet.py │ ├── TODO │ ├── target_data.py │ ├── Logger.py │ ├── io.py │ ├── target_select.py │ ├── MessageStack.py │ ├── browser_config.py │ ├── helpers.py │ ├── SessionManager.py │ └── target_search.py ├── plucky.wav ├── settings.cfg ├── chat_intermediator.py ├── message_blast.py ├── profile_download.py ├── schedule_message.py ├── text_to_speech.py ├── broadcast_message.py ├── get_news.py ├── get_media.py ├── message_timer.py ├── save_chat.py ├── telegram_bot.py ├── about_changer.py ├── online_tracker.py ├── chatbot.py ├── message_service.py ├── target_info.py ├── download_media.py ├── terminal_chat.py └── __main__.py ├── images ├── logo.png ├── usage.png └── infographic.png ├── .gitpod ├── .gitpod.yml └── .gitpod.Dockerfile ├── .travis.yml ├── requirements.txt ├── .github ├── IssueTemplate │ ├── FeatureRequestTemplate.md │ └── BugReportTemplate.md ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── FUNDING.yml ├── Pull_Request_Template.md └── workflows │ ├── pylint.yml │ └── python-app.yml ├── tests ├── test_logger.py ├── test_kill_child_process.py ├── test_helpers.py └── test_func.py ├── Dockerfile ├── LICENSE ├── .circleci └── config.yml ├── setup.py ├── .gitignore ├── CODE_OF_CONDUCT.md ├── README.md └── CONTRIBUTING.md /wplay/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /wplay/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpotter12/whatsapp-play/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/usage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpotter12/whatsapp-play/HEAD/images/usage.png -------------------------------------------------------------------------------- /wplay/plucky.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpotter12/whatsapp-play/HEAD/wplay/plucky.wav -------------------------------------------------------------------------------- /images/infographic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rpotter12/whatsapp-play/HEAD/images/infographic.png -------------------------------------------------------------------------------- /.gitpod/.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - init: pip install -r ./requirements.txt 3 | image: 4 | file: .gitpod.Dockerfile 5 | -------------------------------------------------------------------------------- /wplay/settings.cfg: -------------------------------------------------------------------------------- 1 | [auth] 2 | gmail = alias@gmail.com 3 | passw = yourpassword 4 | devid = 0000000000000000 5 | 6 | [app] 7 | pkg = com.whatsapp 8 | sig = 38a0f7d505fe18fec64fbf343ecaaaf310dbd799 9 | 10 | [client] 11 | pkg = com.google.android.gms 12 | sig = 38918a453d07199354f8b19af05ec6562ced5788 13 | ver = 9877000 -------------------------------------------------------------------------------- /wplay/utils/verify_internet.py: -------------------------------------------------------------------------------- 1 | import http.client as httplib 2 | 3 | 4 | def internet_avalaible(): 5 | """ 6 | Checks internet connection. 7 | """ 8 | conn = httplib.HTTPConnection("www.google.com", timeout=5) 9 | try: 10 | conn.request("HEAD", "/") 11 | conn.close() 12 | return True 13 | except: 14 | conn.close() 15 | return False 16 | -------------------------------------------------------------------------------- /.gitpod/.gitpod.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM custom 2 | 3 | USER gitpod 4 | 5 | # Install custom tools, runtime, etc. using apt-get 6 | # For example, the command below would install "bastet" - a command line tetris clone: 7 | # 8 | # RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/* 9 | # 10 | # More information: https://www.gitpod.io/docs/config-docker/ 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | #sudo: false 4 | 5 | python: 6 | - 3.6 7 | - 3.7 8 | - 3.8 9 | - pypy3 10 | 11 | install: 12 | - python -m pip install -U pip 13 | - pip install -r requirements.txt 14 | - pip install unittest2 15 | - python setup.py install 16 | - pip install coverage 17 | 18 | script: 19 | - python3 -m unittest discover -s tests 20 | - coverage run -m unittest discover -s tests 21 | 22 | after_success: 23 | # - codecov 24 | - bash <(curl -s https://codecov.io/bash) 25 | -------------------------------------------------------------------------------- /wplay/chat_intermediator.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from wplay import terminal_chat 3 | from wplay.utils.Logger import Logger 4 | 5 | from pathlib import Path 6 | # endregion 7 | 8 | # region LOGGER 9 | __logger = Logger(Path(__file__).name) 10 | # endregion 11 | 12 | 13 | async def intermediary(sender, receiver): 14 | """ 15 | Function to create intermediate between two person. 16 | """ 17 | __logger.info("Being and Intermediator") 18 | intermediary.rec = receiver 19 | await terminal_chat.chat(sender) 20 | -------------------------------------------------------------------------------- /wplay/utils/TODO: -------------------------------------------------------------------------------- 1 | #TODO all code: return browser.close and use it instead use exit 2 | 3 | #TODO target_search: Add a whaaaaaat menu 4 | #FIXME target_search: False positive group -> Groups name use the same div as contact status, 5 | so we need to verify if target name is in title, not in status. But, 6 | sometimes the status contain the target name and shows up as 7 | false-positive group. 8 | WHERE WE CAN FIX? __checking_group_list 9 | 10 | 11 | #TODO io: Wait for the last message to be sent before closing the browser -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse>=1.4.0 2 | beautifulsoup4>=4.8.1 3 | colorama>=0.4.3 4 | DateTime>=4.3 5 | decorator>=4.4.2 6 | flake8>=3.7.9 7 | google>=2.0.3 8 | gTTS==2.1.1 9 | newsapi-python>=0.2.6 10 | phonenumbers==8.10.2 11 | playsound>=1.2.2 12 | prompt_toolkit==1.0.14 13 | psutil>=5.7.0 14 | pycodestyle>=2.6.0 15 | pycparser>=2.20 16 | pyee>=7.0.2 17 | pyfiglet>=0.8.post1 18 | pyflakes>=2.2.0 19 | Pygments>=2.6.1 20 | pyppeteer>=0.2.2 21 | python-dotenv>=0.12.0 22 | python-telegram-bot>=12.7 23 | requests>=2.22.0 24 | transitions>=0.7.2 25 | urllib3>=1.25.8 26 | websockets>=8.1 27 | whaaaaat>=0.5.2 28 | -------------------------------------------------------------------------------- /.github/IssueTemplate/FeatureRequestTemplate.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /tests/test_logger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | To run tests run 3 | python3 -m unittest discover -s tests 4 | on terminal 5 | ''' 6 | #region imports 7 | import unittest 8 | from wplay.utils.Logger import Logger 9 | from pathlib import Path 10 | #endregion 11 | 12 | 13 | #region LOGGER create 14 | logger = Logger(Path(__file__).name) 15 | #endregion 16 | 17 | 18 | #region class for Logger function 19 | class CaptureLogsExample(unittest.TestCase): 20 | 21 | def test_assert_logs(self): 22 | """Verify logs using built-in self.assertLogs().""" 23 | logger.error("Testing logg class") 24 | self.assertTrue(logger, 'test_logger.py - ERROR - Testing logg class') 25 | 26 | if __name__ == '__main__': 27 | unittest.main() 28 | #endregion -------------------------------------------------------------------------------- /tests/test_kill_child_process.py: -------------------------------------------------------------------------------- 1 | ''' 2 | To run tests run 3 | python3 -m unittest discover -s tests 4 | on terminal 5 | ''' 6 | #region imports 7 | import unittest 8 | import psutil 9 | import test_func 10 | from wplay.utils.helpers import kill_child_processes 11 | #endregion 12 | 13 | 14 | #region class for test_kill_child_processes function 15 | class Testkill(unittest.TestCase): 16 | 17 | def test_kill(self): 18 | sproc = test_func.get_test_subprocess() 19 | test_pid = sproc.pid 20 | p = psutil.Process(test_pid) 21 | kill_child_processes(test_pid) 22 | p.wait() 23 | self.assertFalse(psutil.pid_exists(test_pid)) 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | #endregion 28 | -------------------------------------------------------------------------------- /.github/IssueTemplate/BugReportTemplate.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to 16 | 2. Click on 17 | 3. Scroll down to 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: 28 | - Browser 29 | - Version 30 | 31 | **Additional context** 32 | Add any other context about the problem here. -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #To build docker image from this file run 2 | #docker build . 3 | #on terminal 4 | 5 | FROM python:3.6-alpine 6 | #LABEL MAINTAINER 7 | 8 | # Copying files 9 | COPY wplay/ /whatsapp-play/wplay 10 | COPY setup.py /whatsapp-play/setup.py 11 | COPY README.md /whatsapp-play/README.md 12 | COPY requirements.txt /whatsapp-play/requirements.txt 13 | 14 | # Dependencies 15 | WORKDIR /whatsapp-play 16 | RUN apk add build-base 17 | RUN apk add make 18 | RUN apk add gcc musl-dev libffi-dev openssl-dev 19 | RUN pip install cryptography==2.9.0 20 | RUN apk add --no-cache libffi-dev 21 | RUN apk add build-base 22 | RUN apk add py3-pip 23 | RUN apk add python3-dev 24 | RUN pip install cffi==1.14.0 25 | RUN pip install -r requirements.txt 26 | 27 | #ENTRYPOINT echo "Hello, welcome to whatsapp-play" 28 | ENTRYPOINT ["python3 -m wplay -h"] 29 | 30 | CMD [ "python"] -------------------------------------------------------------------------------- /wplay/utils/target_data.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | from pyppeteer.page import Page 5 | 6 | from wplay.utils.helpers import whatsapp_selectors_dict 7 | from wplay.utils.Logger import Logger 8 | from wplay.utils.helpers import logs_path 9 | # endregion 10 | 11 | 12 | # region LOGGER 13 | __logger = Logger(Path(__file__).name) 14 | # endregion 15 | 16 | 17 | # region FOR SCRIPTING 18 | async def get_last_seen_from_focused_target(page: Page): 19 | __logger.info("Getting target's status information") 20 | # await page.waitForSelector(whatsapp_selectors_dict['status'], visible = True) 21 | try: 22 | status: str = await page.evaluate(f'document.querySelector("{whatsapp_selectors_dict["last_seen"]}").getAttribute("title")') 23 | return status 24 | except: 25 | return '#status not found' 26 | # endregion 27 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | # repo: rpotter12/whatsapp-play 3 | # filename: FUNDING.YML 4 | 5 | github: [rpotter12] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 6 | patreon: # Replace with a single Patreon username 7 | open_collective: # Replace with a single Open Collective username 8 | ko_fi: # Replace with a single Ko-fi username 9 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 10 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 11 | liberapay: # Replace with a single Liberapay username 12 | issuehunt: # Replace with a single IssueHunt username 13 | otechie: # Replace with a single Otechie username 14 | custom: ["https://paypal.me/rpotter12"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /wplay/message_blast.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | from typing import List 4 | 5 | from wplay.utils import browser_config 6 | from wplay.utils import target_search 7 | from wplay.utils import target_select 8 | from wplay.utils import io 9 | from wplay.utils.Logger import Logger 10 | from wplay.utils.helpers import logs_path 11 | # endregion 12 | 13 | 14 | # region LOGGER 15 | __logger = Logger(Path(__file__).name) 16 | # endregion 17 | 18 | 19 | async def message_blast(target: str): 20 | """ 21 | Sends n number of messages to the target person. 22 | """ 23 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 24 | if target is not None: 25 | await target_search.search_and_select_target_all_ways(page, target) 26 | else: 27 | await target_select.manual_select_target(page) 28 | message: List[str] = io.ask_user_for_message_breakline_mode() 29 | number_of_messages: int = int(input("Enter the number of messages to blast: ")) 30 | __logger.debug("Blasting messages") 31 | for _ in range(number_of_messages): 32 | await io.send_message(page, message) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rohit Potter 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 | -------------------------------------------------------------------------------- /.github/Pull_Request_Template.md: -------------------------------------------------------------------------------- 1 | ## Issue that this pull request solves 2 | 3 | Closes: # (issue number) 4 | 5 | ## Proposed changes 6 | 7 | Brief description of what is fixed or changed 8 | 9 | ## Types of changes 10 | 11 | _Put an `x` in the boxes that apply_ 12 | 13 | - [ ] Bugfix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] Documentation update (Documentation content changed) 17 | - [ ] Other (please describe): 18 | 19 | ## Checklist 20 | 21 | _Put an `x` in the boxes that apply_ 22 | 23 | - [ ] My code follows the style guidelines of this project 24 | - [ ] I have performed a self-review of my own code 25 | - [ ] I have commented my code, particularly in hard-to-understand areas 26 | - [ ] I have made corresponding changes to the documentation 27 | - [ ] My changes generate no new warnings 28 | 29 | ## Screenshots 30 | 31 | Please attach the screenshots of the changes made in case of change in user interface 32 | 33 | ## Other information 34 | 35 | Any other information that is important to this pull request 36 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.8 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install pylint 20 | - name: Analysing the code with pylint 21 | run: | 22 | # pylint `ls -R|grep .py$|xargs` 23 | pylint wplay/about_changer.py 24 | pylint wplay/broadcast_message.py 25 | pylint wplay/chat_intermediator.py 26 | pylint wplay/chatbot.py 27 | pylint wplay/download_media.py 28 | pylint wplay/get_media.py 29 | pylint wplay/get_news.py 30 | pylint wplay/message_blast.py 31 | pylint wplay/message_service.py 32 | pylint wplay/message_timer.py 33 | pylint wplay/online_tracker.py 34 | pylint wplay/profile_download.py 35 | pylint wplay/save_chat.py 36 | pylint wplay/schedule_message.py 37 | pylint wplay/target_info.py 38 | pylint wplay/telegram_bot.py 39 | pylint wplay/terminal_chat.py 40 | pylint wplay/text_to_speech.py 41 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python application 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up Python 3.9 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: 3.9 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install flake8 pytest 27 | pip install wplay 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | - name: Lint with flake8 30 | run: | 31 | # stop the build if there are Python syntax errors or undefined names 32 | # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 33 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 34 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 35 | - name: Test with pytest 36 | run: | 37 | pytest 38 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | ''' 2 | To run tests run 3 | python3 -m unittest discover -s tests 4 | on terminal 5 | ''' 6 | #region imports 7 | import unittest 8 | from os import path 9 | from wplay.utils import helpers 10 | #endregion 11 | 12 | 13 | #region class 14 | class testPath(unittest.TestCase): 15 | 16 | def test_paths_exist(self): 17 | # self.assertTrue(path.exists(helpers.audio_file_folder_path)) 18 | # self.assertTrue(path.exists(helpers.chatbot_image_folder_path)) 19 | self.assertTrue(path.exists(helpers.data_folder_path)) 20 | self.assertTrue(path.exists(helpers.logs_path)) 21 | self.assertTrue(path.exists(helpers.log_file_path)) 22 | # self.assertTrue(path.exists(helpers.media_path)) 23 | # self.assertTrue(path.exists(helpers.messages_json_folder_path)) 24 | # self.assertTrue(path.exists(helpers.messages_json_path)) 25 | # self.assertTrue(path.exists(helpers.open_messages_json_path)) 26 | # self.assertTrue(path.exists(helpers.profile_photos_path)) 27 | # self.assertTrue(path.exists(helpers.save_chat_folder_path)) 28 | self.assertTrue(path.exists(helpers.test_log_file_path)) 29 | # self.assertTrue(path.exists(helpers.tracking_folder_path)) 30 | # self.assertTrue(path.exists(helpers.user_data_folder_path)) 31 | 32 | if __name__ == '__main__': 33 | unittest.main() 34 | #endregion 35 | -------------------------------------------------------------------------------- /wplay/profile_download.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | from wplay.utils import browser_config 5 | from wplay.utils import target_search 6 | from wplay.utils import target_select 7 | from wplay.utils.helpers import profile_photos_path 8 | from wplay.utils.helpers import whatsapp_selectors_dict 9 | from wplay.utils.Logger import Logger 10 | # endregion 11 | 12 | 13 | # region LOGGER 14 | __logger = Logger(Path(__file__).name) 15 | # endregion 16 | 17 | 18 | async def get_profile_picture(target): 19 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 20 | 21 | if target is not None: 22 | try: 23 | await target_search.search_and_select_target(page, target) 24 | except Exception as e: 25 | print(e) 26 | await page.reload() 27 | await target_search.search_and_select_target_without_new_chat_button(page, target) 28 | else: 29 | target = await target_select.manual_select_target(page) 30 | # Getting Profile picture url 31 | selector = '#main > header > div > div > img' 32 | await page.waitForSelector(selector, timeout=2000) 33 | image_url = await page.evaluate(f'document.querySelector("{selector}").getAttribute("src")') 34 | try: 35 | viewSource = await page.goto(image_url) 36 | f = open(profile_photos_path / f'{target}.jpg', 'wb') 37 | f.write(await viewSource.buffer()) 38 | f.close() 39 | except Exception as e: 40 | print("Error saving image") 41 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Python CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-python/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/python:3.7-node-browsers-legacy 10 | 11 | steps: 12 | - checkout 13 | 14 | # Download and cache dependencies 15 | - restore_cache: 16 | keys: 17 | - v1-dependencies-{{ checksum "requirements.txt" }} 18 | # fallback to using the latest cache if no exact match is found 19 | - v1-dependencies- 20 | 21 | - run: 22 | name: install dependencies 23 | command: | 24 | python3 -m venv venv 25 | . venv/bin/activate 26 | pip install --upgrade pip 27 | pip install -r requirements.txt 28 | 29 | - save_cache: 30 | paths: 31 | - ./venv 32 | key: v1-dependencies-{{ checksum "requirements.txt" }} 33 | 34 | - run: 35 | name: run tests 36 | command: | 37 | . venv/bin/activate 38 | python3 -m unittest discover -s tests 39 | 40 | # name: run linting and metrics 41 | # command: | 42 | # . venv/bin/activate 43 | # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=500 --statistics --ignore=C901,E251,E722,E231,E902 --exclude=.git,.venv,.gitignore 44 | 45 | - store_artifacts: 46 | path: test-reports 47 | destination: test-reports 48 | -------------------------------------------------------------------------------- /wplay/schedule_message.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from datetime import datetime 3 | from pathlib import Path 4 | import time 5 | import sys 6 | 7 | from wplay.utils import browser_config 8 | from wplay.utils import target_search 9 | from wplay.utils import target_select 10 | from wplay.utils import io 11 | from wplay.utils.Logger import Logger 12 | # endregion 13 | 14 | 15 | # region LOGGER 16 | __logger = Logger(Path(__file__).name) 17 | # endregion 18 | 19 | 20 | async def schedule_message(target): 21 | """ 22 | Sends message to the target person at a scheduled time. 23 | """ 24 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 25 | if target is not None: 26 | await target_search.search_and_select_target(page, target) 27 | else: 28 | await target_select.manual_select_target(page) 29 | time_ = input("Enter the schedule time in HH:MM:SS format-> ") 30 | hour, minute, second = time_.split(':') 31 | current_time = datetime.now() 32 | delta_hour: int = int(hour) - current_time.hour 33 | delta_min: int = int(minute) - current_time.minute 34 | delta_second: int = int(second) - current_time.second 35 | total_seconds: int = delta_hour*3600 + delta_min*60 + delta_second 36 | if total_seconds < 0: 37 | print("Current time is ahead of the scheduled time") 38 | sys.exit() 39 | message: list[str] = io.ask_user_for_message_breakline_mode() 40 | print("Your message is scheduled at : ", time_) 41 | time.sleep(total_seconds) 42 | await io.send_message(page, message) 43 | -------------------------------------------------------------------------------- /wplay/text_to_speech.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | from wplay.utils.helpers import audio_file_folder_path 5 | from wplay.utils.Logger import Logger 6 | 7 | from gtts import gTTS 8 | # endregion 9 | 10 | 11 | # region LOGGER 12 | __logger = Logger(Path(__file__).name) 13 | # endregion 14 | 15 | 16 | async def text_to_speech(target): 17 | 18 | try: 19 | __logger.info("Converting text to speech audio file") 20 | # The text that you want to convert to audio 21 | text = input("\n\nWrite the text you want to convert to audio file: ") 22 | 23 | list_laguages = ['bn: Bengali', 'de: German', 'en: English', 'es: Spanish', 'fr: French', 'gu: Gujarati', 'hi: Hindi', 24 | 'it: Italian', 'ja: Japanese', 'kn: Kannada', 'ko: Korean', 'ml: Malayalam', 'mr: Marathi', 'pt-br: Portuguese (Brazil)', 'ru: Russian', 'ta: Tamil', 'te: Telugu', 'ur: Urdu'] 25 | 26 | print('Choose a code for language of your choice from the following list\n') 27 | print(list_laguages) 28 | 29 | # Language in which you want to convert 30 | language = input("\n\nEnter the language you want to convert to audio file: ") 31 | 32 | # Passing the text and language to the engine, 33 | myobj = gTTS(text=text, lang=language, slow=False) 34 | # Saving the converted audio in a mp3 file named 35 | myobj.save(audio_file_folder_path / "{}.mp3".format(target)) 36 | 37 | except Exception as e: 38 | print(e) 39 | 40 | finally: 41 | print('\nAudio file saved in: {}/ {}.mp3'.format(audio_file_folder_path, target)) 42 | -------------------------------------------------------------------------------- /wplay/utils/Logger.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | import logging 3 | from pathlib import Path 4 | from wplay.utils.helpers import log_file_path, logs_path, test_log_file_path 5 | # endregion 6 | 7 | 8 | class Logger: 9 | if not (log_file_path).exists(): 10 | logs_path.mkdir(parents=True, exist_ok=True) 11 | open(log_file_path, 'w').close() 12 | 13 | if not (test_log_file_path).exists(): 14 | logs_path.mkdir(parents=True, exist_ok=True) 15 | open(test_log_file_path, 'w').close() 16 | 17 | def __init__(self, script_name: str, level: int = logging.WARNING): 18 | self.logger = logging.getLogger(script_name) 19 | self.level = level 20 | self.logger.setLevel(self.level) 21 | 22 | if not self.logger.handlers: 23 | # Create handlers 24 | file_handler = logging.FileHandler(log_file_path) 25 | file_handler = logging.FileHandler(test_log_file_path) 26 | console = logging.StreamHandler() 27 | file_handler.setLevel(self.level) 28 | console.setLevel(self.level) 29 | 30 | # create formatter and add it to the handlers 31 | formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s') 32 | console.setFormatter(formatter) 33 | file_handler.setFormatter(formatter) 34 | 35 | # add the handlers to logger 36 | self.logger.addHandler(console) 37 | self.logger.addHandler(file_handler) 38 | 39 | def debug(self, msg: str): 40 | self.logger.debug(msg) 41 | 42 | def error(self, msg: str): 43 | self.logger.error(msg) 44 | 45 | def info(self, msg: str): 46 | self.logger.info(msg) 47 | -------------------------------------------------------------------------------- /wplay/broadcast_message.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from tkinter import Tk 3 | from tkinter.filedialog import askopenfile 4 | from pathlib import Path 5 | 6 | from wplay.utils import browser_config 7 | from wplay.utils.target_search import search_target_by_number 8 | from wplay.utils import io 9 | from typing import List 10 | from wplay.utils.helpers import data_folder_path 11 | from wplay.utils.Logger import Logger 12 | # endregion 13 | 14 | # region LOGGER 15 | __logger = Logger(Path(__file__).name) 16 | # endregion 17 | 18 | 19 | class InvalidNumber(Exception): 20 | message = "Either Number is invalid or no account exist for the number or the number was kept in wrong format :(\n" 21 | 22 | 23 | def ProcessNumbers(): 24 | __logger.info("Processing numbers.") 25 | print("Choose a text file containing full numbers with country code, one number per line.") 26 | Tk().withdraw() 27 | filename = askopenfile( 28 | initialdir=data_folder_path, 29 | title='Choose a text file with numbers.', 30 | filetypes=[("text files", "*.txt")], 31 | mode="r" 32 | ) 33 | numbers = filename.readlines() 34 | for i in range(len(numbers)): 35 | number = numbers[i].strip("\n+") 36 | numbers[i] = number 37 | return numbers 38 | 39 | 40 | async def broadcast(): 41 | __logger.info("Broadcast message.") 42 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 43 | numbers = ProcessNumbers() 44 | message: List[str] = io.ask_user_for_message_breakline_mode() 45 | 46 | for number in numbers: 47 | if await search_target_by_number(page, number): 48 | await io.send_message(page, message) 49 | 50 | __logger.info("Messages broadcasted successfully!") 51 | print("Messages broadcasted successfully!") 52 | -------------------------------------------------------------------------------- /wplay/get_news.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | import time 4 | 5 | from newsapi.newsapi_client import NewsApiClient 6 | 7 | from wplay.utils import browser_config 8 | from wplay.utils import target_search 9 | from wplay.utils import target_select 10 | from wplay.utils import io 11 | from wplay.utils.Logger import Logger 12 | # endregion 13 | 14 | 15 | # region LOGGER 16 | __logger = Logger(Path(__file__).name) 17 | # endregion 18 | 19 | ''' 20 | Visit https://newsapi.org/ to get your own API key. 21 | ''' 22 | newsapi = NewsApiClient(api_key="YOUR API KEY") 23 | 24 | async def get_news(target): 25 | """ 26 | Sends news as a message in every two minutes. 27 | """ 28 | def fetch_news(country_code): 29 | """ 30 | Return the title and url of the news. 31 | """ 32 | headlines = newsapi.get_top_headlines(country=country_code, language='en') 33 | url = headlines['articles'][0]['url'] 34 | title = headlines['articles'][0]['title'] 35 | return title, url 36 | 37 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 38 | if target is not None: 39 | try: 40 | await target_search.search_and_select_target(page, target) 41 | except Exception as e: 42 | print(e) 43 | await target_search.search_and_select_target_without_new_chat_button(page, target) 44 | else: 45 | await target_select.manual_select_target(page) 46 | 47 | country = input("Enter your country code (ex: us or in): ") 48 | while True: 49 | try: 50 | news, source = fetch_news(country) 51 | news_ = f"*{news}* \n Full News : {source}" 52 | await io.send_message(page, news_) 53 | except Exception as e: 54 | print("Unable to get the news", e) 55 | time.sleep(120) # Sends news in every 2 min 56 | -------------------------------------------------------------------------------- /wplay/utils/io.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | from typing import List, Union 4 | 5 | from pyppeteer.page import Page 6 | 7 | from wplay.utils.helpers import whatsapp_selectors_dict 8 | from wplay.utils.Logger import Logger 9 | from wplay.utils.helpers import logs_path 10 | # endregion 11 | 12 | 13 | # region LOGGER 14 | __logger = Logger(Path(__file__).name) 15 | # endregion 16 | 17 | 18 | # region FOR SCRIPTING 19 | def ask_user_for_message() -> str: 20 | return str(input("Write your message: ")) 21 | 22 | 23 | def ask_user_for_message_breakline_mode() -> List[str]: 24 | message = list() 25 | i = 0 26 | print("Write your message (Enter key to breakline)('.' alone to send):") 27 | while True: 28 | message.append(str(input())) 29 | if message[i] == '.': 30 | message.pop(i) 31 | break 32 | elif message[i] == '...' or message[i] == '#_FILE' or message[i] == '#_TTS' or message[i] == '#_FWD': 33 | break 34 | i += 1 35 | return message 36 | 37 | 38 | async def send_message(page: Page, message: Union[List[str], str]): 39 | __logger.debug("Sending message") 40 | for i in range(len(message)): 41 | await page.type(whatsapp_selectors_dict['message_area'], message[i]) 42 | if isinstance(message, list): 43 | await page.keyboard.down('Shift') 44 | await page.keyboard.press('Enter') 45 | await page.keyboard.up('Shift') 46 | await page.keyboard.press('Enter') 47 | 48 | 49 | async def send_file(page): 50 | __logger.info("Sending File") 51 | await page.click(whatsapp_selectors_dict['attach_file']) 52 | await page.click(whatsapp_selectors_dict['choose_file']) 53 | await page.waitForSelector(whatsapp_selectors_dict['send_file'], timeout=30000) 54 | await page.click(whatsapp_selectors_dict['send_file']) 55 | # endregion 56 | -------------------------------------------------------------------------------- /wplay/get_media.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | import time 3 | from pathlib import Path 4 | 5 | from wplay.utils import browser_config 6 | from wplay.utils.Logger import Logger 7 | from wplay.utils.helpers import profile_photos_path 8 | # endregion 9 | 10 | 11 | # region LOGGER 12 | __logger = Logger(Path(__file__).name) 13 | # endregion 14 | 15 | 16 | async def get_profile_photos(): 17 | """ 18 | Download the profile picture of all the contacts. 19 | """ 20 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 21 | total_contacts = int(input("Please provide total whatsapp contacts: ")) 22 | loop = round(total_contacts/7) 23 | images_list = [] 24 | 25 | await page.waitForSelector('#pane-side > div:nth-child(1) > div > div > div:nth-child(1) > div > div > div > div > img') 26 | 27 | for c in range(loop): 28 | for i in range(1, 18): 29 | selector = f"#pane-side > div:nth-child(1) > div > div > div:nth-child({i}) > div > div > div > div > img" 30 | try: 31 | await page.waitForSelector(selector, timeout=2000) 32 | image_url = await page.evaluate(f'document.querySelector("{selector}").getAttribute("src")') 33 | print(f"{c}:{i}-{image_url}") 34 | if image_url not in images_list: 35 | images_list.append(image_url) 36 | except Exception as e: 37 | print(e) 38 | print("No profile image found") 39 | await page.evaluate("document.querySelector('#pane-side').scrollBy(0, 500)") 40 | 41 | for count in range(len(images_list)): 42 | try: 43 | viewSource = await page.goto(images_list[count]) 44 | f = open(profile_photos_path / f'{count}.jpg', 'wb') 45 | f.write(await viewSource.buffer()) 46 | f.close() 47 | except Exception as e: 48 | print("Error saving image") 49 | 50 | print("Saved all the images to media_folder.") 51 | time.sleep(5) 52 | -------------------------------------------------------------------------------- /wplay/message_timer.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | import time 3 | import random 4 | from pathlib import Path 5 | 6 | from wplay.utils import browser_config 7 | from wplay.utils import target_search 8 | from wplay.utils import target_select 9 | from wplay.utils import io 10 | from wplay.utils.Logger import Logger 11 | from wplay.utils.helpers import logs_path 12 | # endregion 13 | 14 | 15 | # region LOGGER 16 | __logger = Logger(Path(__file__).name) 17 | # endregion 18 | 19 | 20 | async def message_timer(target): 21 | """ 22 | Sends message in a particular time interval. 23 | """ 24 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 25 | if target is not None: 26 | try: 27 | await target_search.search_and_select_target(page, target) 28 | except Exception as e: 29 | print(e) 30 | await page.reload() 31 | await target_search.search_and_select_target_without_new_chat_button(page, target) 32 | else: 33 | await target_select.manual_select_target(page) 34 | # Region inputs 35 | __logger.info("Input message information for message timer") 36 | message_type_numbers: int = int(input("How many types of messages will you send? ")) 37 | messages: list[str] = list() 38 | for _ in range(message_type_numbers): 39 | messages.append(io.ask_user_for_message_breakline_mode()) 40 | number_of_messages: int = int(input("Enter the number of messages to send: ")) 41 | minimumTimeInterval: int = int(input("Enter minimum interval number in seconds: ")) 42 | maximumTimeInterval: int = int(input("Enter maximum interval number in seconds: ")) 43 | # Endregion 44 | 45 | random.seed() 46 | for _ in range(number_of_messages): 47 | if not messages: 48 | break 49 | await io.send_message(page, messages[random.randrange(0, message_type_numbers)]) 50 | if minimumTimeInterval != maximumTimeInterval: 51 | time.sleep(random.randrange(minimumTimeInterval, maximumTimeInterval)) 52 | else: 53 | time.sleep(minimumTimeInterval) 54 | -------------------------------------------------------------------------------- /wplay/save_chat.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | from wplay.utils import browser_config 5 | from wplay.utils import target_search 6 | from wplay.utils import target_select 7 | from wplay.utils.helpers import save_chat_folder_path 8 | from wplay.utils.Logger import Logger 9 | # endregion 10 | 11 | 12 | # region LOGGER 13 | __logger = Logger(Path(__file__).name) 14 | # endregion 15 | 16 | 17 | async def save_chat(target): 18 | """ 19 | Save the whole chat of the target person in .txt file. 20 | """ 21 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 22 | 23 | if target is not None: 24 | try: 25 | await target_search.search_and_select_target(page, target) 26 | except Exception as e: 27 | print(e) 28 | await page.reload() 29 | await target_search.search_and_select_target_without_new_chat_button(page, target) 30 | else: 31 | target = await target_select.manual_select_target(page) 32 | 33 | # selectors 34 | selector_values = "#main > div > div > div > div > div > div > div > div" 35 | selector_sender = "#main > div > div > div > div > div > div > div > div > div.copyable-text" 36 | 37 | # Getting all the messages of the chat 38 | try: 39 | __logger.info("Saving chats with target") 40 | await page.waitForSelector(selector_values) 41 | values = await page.evaluate(f'''() => [...document.querySelectorAll('{selector_values}')] 42 | .map(element => element.textContent)''') 43 | sender = await page.evaluate(f'''() => [...document.querySelectorAll('{selector_sender}')] 44 | .map(element => element.getAttribute("data-pre-plain-text"))''') 45 | 46 | final_values = [x[:-8] for x in values] 47 | new_list = [a + b for a, b in zip(sender, final_values)] 48 | 49 | # opens chat file of the target person 50 | with open(save_chat_folder_path / f'chat_{target}.txt', 'w') as output: 51 | for s in new_list: 52 | output.write("%s\n" % s) 53 | 54 | except Exception as e: 55 | print(e) 56 | 57 | finally: 58 | # save the chat and close the file 59 | output.close() 60 | print(f'\nChat file saved in: {str(save_chat_folder_path/"chat_")}{target}.txt') 61 | -------------------------------------------------------------------------------- /wplay/utils/target_select.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | from pyppeteer.page import Page 5 | 6 | from wplay.utils import target_search 7 | from wplay.utils.Logger import Logger 8 | from wplay.utils.helpers import whatsapp_selectors_dict 9 | # endregion 10 | 11 | 12 | # region FOR SCRIPTING 13 | async def manual_select_target(page: Page, hide_groups: bool = False): 14 | __print_manual_selection_info() 15 | await __open_new_chat(page) 16 | target_focused_title = await __get_focused_target_title(page) 17 | await __wait_for_message_area(page) 18 | __print_selected_target_title(target_focused_title) 19 | complete_target_info = await target_search.__get_complete_info_on_target(page) 20 | target_search.__print_complete_target_info(complete_target_info) 21 | await __close_contact_info_page(page) 22 | return target_focused_title 23 | # endregion 24 | 25 | 26 | # region SELECT TARGET 27 | def __print_manual_selection_info(): 28 | print(f"You've to go to whatsapp web and select target manually") 29 | 30 | 31 | def __print_selected_target_title(target_focused_title: str): 32 | print(f"You've selected the target named by: {target_focused_title}") 33 | 34 | 35 | async def __close_contact_info_page(page: Page): 36 | try: 37 | await page.waitForSelector( 38 | whatsapp_selectors_dict['contact_info_page_close_button'], 39 | visible=True, 40 | timeout=5000 41 | ) 42 | await page.click(whatsapp_selectors_dict['contact_info_page_close_button']) 43 | except Exception as e: 44 | print(e) 45 | 46 | 47 | async def __open_new_chat(page: Page): 48 | await page.waitForSelector( 49 | whatsapp_selectors_dict['new_chat_button'], 50 | visible=True, 51 | timeout=0 52 | ) 53 | 54 | 55 | async def __get_focused_target_title(page: Page): 56 | try: 57 | await page.waitForSelector(whatsapp_selectors_dict['target_focused_title'], visible=True, timeout=0) 58 | target_focused_title = await page.evaluate(f'document.querySelector("{whatsapp_selectors_dict["target_focused_title"]}").getAttribute("title")') 59 | except Exception as e: 60 | print(f'No target selected! Error: {str(e)}') 61 | exit() 62 | return target_focused_title 63 | 64 | 65 | async def __wait_for_message_area(page: Page): 66 | try: 67 | await page.waitForSelector(whatsapp_selectors_dict['message_area'], timeout=0) 68 | except Exception as e: 69 | print(f"You don't belong this group anymore! Error: {str(e)}") 70 | # endregion 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | setup( 7 | name="wplay", 8 | version="8.0.7", 9 | install_requires=["argparse >= 1.4.0", 10 | "beautifulsoup4>=4.8.1", 11 | "colorama>=0.4.3", 12 | "dateTime>=4.3", 13 | "decorator>=4.4.2", 14 | "flake8>=3.7.9", 15 | "google>=2.0.3", 16 | "gTTS==2.1.1", 17 | "newsapi-python>=0.2.6", 18 | "phonenumbers==8.10.2", 19 | "playsound>=1.2.2", 20 | "prompt_toolkit==1.0.14", 21 | "psutil>=5.7.0", 22 | "pyfiglet>=0.8.post1", 23 | "pyflakes>=2.2.0", 24 | "Pygments>=2.6.1", 25 | "pyppeteer>=0.0.25", 26 | "python-dotenv==0.12.0", 27 | "python-telegram-bot>=11.1.0", 28 | "requests>=2.22.0", 29 | "transitions>=0.7.2", 30 | "urllib3>=1.25.8", 31 | "websockets>=8.1", 32 | "whaaaaat>=0.5.2", 33 | ], 34 | packages=find_packages(), 35 | description="command line software to play with your WhatsApp", 36 | long_description=long_description, 37 | long_description_content_type="text/markdown", 38 | author="Rohit Potter, Alexandre Calil", 39 | author_email="rohitpotter12@gmail.com, alexandrecalilmf@gmail.com", 40 | license="MIT", 41 | python_requires=">=3.6", 42 | url="https://github.com/rpotter12/whatsapp-play/", 43 | download_url="https://pypi.org/project/wplay/", 44 | keywords=[ 45 | "whatsapp", 46 | "whatsapp cli", 47 | "whatsapp api", 48 | "whatsapp service" 49 | "whatsapp chat", 50 | "message blast", 51 | "message timer", 52 | "whatsapp terminal", 53 | "whatsapp news", 54 | "whatsapp schedule", 55 | "tracker", 56 | "online tracking", 57 | "save-chat" 58 | ], 59 | classifiers=[ 60 | "Programming Language :: Python", 61 | "Programming Language :: Python :: 3", 62 | "Programming Language :: Python :: 3.6", 63 | "Programming Language :: Python :: 3.7", 64 | "Programming Language :: Python :: 3.8", 65 | "Programming Language :: Python :: 3 :: Only", 66 | "License :: OSI Approved :: MIT License", 67 | "Operating System :: OS Independent", 68 | ], 69 | entry_points={"console_scripts": ["wplay = wplay.__main__:main"]}, 70 | ) 71 | -------------------------------------------------------------------------------- /wplay/telegram_bot.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | import tkinter 3 | from tkinter import filedialog 4 | from pathlib import Path 5 | import pickle 6 | 7 | from telegram.ext import CommandHandler, Updater 8 | from wplay.utils.helpers import data_folder_path 9 | from wplay.utils.Logger import Logger 10 | # endregion 11 | 12 | 13 | # region LOGGER 14 | __logger = Logger(Path(__file__).name) 15 | # endregion 16 | 17 | 18 | status_file_path = None 19 | 20 | 21 | def start_tkinter(): 22 | root_window = tkinter.Tk() 23 | root_window.withdraw() 24 | 25 | 26 | def ask_where_are_the_status_file(): 27 | print('Choose a status text file.') 28 | status_file_path = filedialog.askopenfile( 29 | initialdir=data_folder_path / 'tracking_data', 30 | title='Choose a status text file.', 31 | filetypes=(("text files", "*.txt"), ("all files", "*.*")) 32 | ) 33 | if status_file_path == (): 34 | print("Error! Choose a status.") 35 | exit() 36 | return status_file_path 37 | 38 | 39 | def startmessage(bot, update): 40 | chat_id: int = update.message.chat_id 41 | text: str = ''' 42 | Hi, I am here to send all tracked online status in whatsapp :) 43 | ''' 44 | bot.send_message(chat_id=chat_id, text=text) 45 | 46 | 47 | def send_status(bot, update): 48 | # Display last updated online status message 49 | chat_id = update.message.chat_id 50 | try: 51 | f = open(status_file_path, 'r') 52 | file_data = f.readlines() 53 | text: Union[str, bytes] = file_data[len(file_data) - 1] 54 | bot.send_message(chat_id=chat_id, text=text) 55 | except Exception as e: 56 | print(e) 57 | bot.send_message(chat_id=chat_id, text='oops! An error occurred') 58 | 59 | 60 | def telegram_status(name): 61 | print(name) 62 | start_tkinter() 63 | global status_file_path 64 | status_file_path = ask_where_are_the_status_file() 65 | # Add bot token 66 | global TOKEN 67 | new_token = False 68 | token_file_path = "wplay/telegram_token.pkl" 69 | if Path(token_file_path).exists(): 70 | user_choice = input("Do you want to use last saved token (Y) or enter new token (N): ") 71 | if user_choice in "Yy": 72 | with open(token_file_path, "rb") as token_file: 73 | TOKEN = pickle.load(token_file) 74 | else: 75 | new_token = True 76 | else: 77 | new_token = True 78 | if new_token: 79 | TOKEN = input("Enter token: ") 80 | with open(token_file_path, "wb") as token_file: 81 | pickle.dump(TOKEN, token_file) 82 | 83 | # Added all the essential command handlers 84 | updater = Updater(TOKEN, use_context=True) 85 | dp = updater.dispatcher 86 | dp.add_handler(CommandHandler('start', startmessage)) 87 | dp.add_handler(CommandHandler('status', send_status)) 88 | updater.start_polling() 89 | updater.idle() 90 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Personal Data 2 | .userData 3 | tracking_data 4 | data 5 | telegram_token.pkl 6 | 7 | # Byte-compiled / optimized / DLL files 8 | wplay.egg-info/ 9 | __pycache__/ 10 | *.pyc 11 | __pycache__/ 12 | src/\.DS_Store 13 | src/__pycache__/ 14 | src/logs/ 15 | src/test/__pycache__/ 16 | \.DS_Store 17 | \.idea/ 18 | src/XDG_CACHE_HOME/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | ImplementationTests 28 | build/ 29 | develop-eggs/ 30 | dist/ 31 | downloads/ 32 | eggs/ 33 | .eggs/ 34 | lib/ 35 | lib64/ 36 | parts/ 37 | sdist/ 38 | var/ 39 | wheels/ 40 | pip-wheel-metadata/ 41 | share/python-wheels/ 42 | *.egg-info/ 43 | .installed.cfg 44 | *.egg 45 | MANIFEST 46 | 47 | # PyInstaller 48 | # Usually these files are written by a python script from a template 49 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 50 | *.manifest 51 | *.spec 52 | 53 | # Installer logs 54 | pip-log.txt 55 | pip-delete-this-directory.txt 56 | 57 | # Unit test / coverage reports 58 | htmlcov/ 59 | .tox/ 60 | .nox/ 61 | .coverage 62 | .coverage.* 63 | .cache 64 | nosetests.xml 65 | coverage.xml 66 | *.cover 67 | .hypothesis/ 68 | .pytest_cache/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | target/ 92 | 93 | # Jupyter Notebook 94 | .ipynb_checkpoints 95 | 96 | # IPython 97 | profile_default/ 98 | ipython_config.py 99 | 100 | # pyenv 101 | .python-version 102 | 103 | # pipenv 104 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 105 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 106 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 107 | # install all needed dependencies. 108 | #Pipfile.lock 109 | 110 | # celery beat schedule file 111 | celerybeat-schedule 112 | 113 | # SageMath parsed files 114 | *.sage.py 115 | 116 | # Environments 117 | .env 118 | .venv 119 | env/ 120 | venv/ 121 | ENV/ 122 | env.bak/ 123 | venv.bak/ 124 | 125 | # Spyder project settings 126 | .spyderproject 127 | .spyproject 128 | 129 | # Rope project settings 130 | .ropeproject 131 | 132 | # mkdocs documentation 133 | /site 134 | 135 | # mypy 136 | .mypy_cache/ 137 | .dmypy.json 138 | dmypy.json 139 | 140 | # Pyre type checker 141 | .pyre/ 142 | 143 | # Visual Studio 144 | .vs 145 | 146 | # Visual Studio Code 147 | .vscode 148 | 149 | 150 | # React 151 | # dependencies 152 | /node_modules 153 | /.pnp 154 | .pnp.js 155 | 156 | # testing 157 | /coverage 158 | 159 | # production 160 | /build 161 | 162 | # misc 163 | .DS_Store 164 | .env.local 165 | .env.development.local 166 | .env.test.local 167 | .env.production.local 168 | 169 | npm-debug.log* 170 | yarn-debug.log* 171 | yarn-error.log* 172 | -------------------------------------------------------------------------------- /wplay/about_changer.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | import time 3 | from pathlib import Path 4 | from wplay.utils.helpers import whatsapp_selectors_dict 5 | from wplay.utils import browser_config 6 | from wplay.utils.Logger import Logger 7 | from newsapi.newsapi_client import NewsApiClient 8 | # endregion 9 | 10 | 11 | # region LOGGER 12 | __logger = Logger(Path(__file__).name) 13 | # endregion 14 | 15 | 16 | # Asking user 17 | async def about_changer(): 18 | option = input("Choose(1/2) \n1.Write new about \n2.Change about with latest headline\n") 19 | if option == '1': 20 | await change_about() 21 | else: 22 | await about_changer_news() 23 | 24 | 25 | # Custom About 26 | async def change_about(): 27 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 28 | # opens photo element 29 | await page.waitForSelector(whatsapp_selectors_dict['profile_photo_element'], visible=True) 30 | await page.click(whatsapp_selectors_dict['profile_photo_element']) 31 | await page.waitForSelector(whatsapp_selectors_dict['about_edit_button_element']) 32 | await page.click(whatsapp_selectors_dict['about_edit_button_element']) 33 | status = input("Enter your new about: ") 34 | __logger.info("Writing About") 35 | # Write about 36 | await page.type(whatsapp_selectors_dict['about_text_area'], status) 37 | await page.keyboard.press('Enter') 38 | print("About changed to {}".format(status)) 39 | 40 | 41 | async def get_api_key(): 42 | __logger.info("Getting key") 43 | print("Visit https://newsapi.org/ to get your own API key") 44 | key = input("Enter you API KEY : ") 45 | get_api_key.newsapi = NewsApiClient(api_key='{}'.format(key)) 46 | 47 | 48 | # News in About 49 | async def about_changer_news(): 50 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 51 | await get_api_key() 52 | query: str = str(input("What's the news theme? : ")) 53 | # opens photo element 54 | await page.waitForSelector(whatsapp_selectors_dict['profile_photo_element'], visible=True) 55 | await page.click(whatsapp_selectors_dict['profile_photo_element']) 56 | news = '' 57 | while True: 58 | current_news = str(fetch_news(query)) 59 | print(current_news) 60 | if news != current_news: 61 | # Click on edit about button 62 | await page.waitForSelector(whatsapp_selectors_dict['about_edit_button_element']) 63 | await page.click(whatsapp_selectors_dict['about_edit_button_element']) 64 | for _ in range(140): 65 | await page.keyboard.press('Backspace') 66 | news = current_news 67 | __logger.info("Updating About latest news") 68 | # Write about 69 | await page.type(whatsapp_selectors_dict['about_text_area'], news) 70 | await page.keyboard.press('Enter') 71 | # News get updated every 15 minutes by newsapi.org 72 | # Added extra minute for buffer period 73 | # For free account : max limit is 500 request/day 74 | time.sleep(905) 75 | 76 | 77 | def fetch_news(query): 78 | __logger.info("Fetching news") 79 | top_headlines = get_api_key.newsapi.get_top_headlines(q=query, language='en') 80 | return top_headlines['articles'][0]['title'] 81 | -------------------------------------------------------------------------------- /wplay/online_tracker.py: -------------------------------------------------------------------------------- 1 | import time 2 | from pathlib import Path 3 | from datetime import datetime 4 | from playsound import playsound 5 | from wplay.utils import browser_config 6 | from wplay.utils import target_search 7 | from wplay.utils import target_select 8 | from wplay.utils import target_data 9 | from wplay.utils.helpers import tracking_folder_path 10 | from wplay.utils.Logger import Logger 11 | from wplay.utils.helpers import logs_path 12 | 13 | 14 | # region LOGGER 15 | __logger = Logger(Path(__file__).name) 16 | # endregion 17 | 18 | 19 | async def tracker(target): 20 | """ 21 | This function checks the online and offline status of the target person. 22 | """ 23 | page, _ = await browser_config.configure_browser_and_load_whatsapp() # open bot browser and load whatsapp web website 24 | if target is not None: # checks if target is not none then it search for the target and select it 25 | try: 26 | target_name = await target_search.search_and_select_target(page, target, hide_groups=True) 27 | except Exception as e: 28 | print(e) 29 | await page.reload() 30 | target_name = await target_search.search_and_select_target_without_new_chat_button(page, target, hide_groups=True) 31 | else: # if target is none then it allow user to select target manually from browser 32 | target_name = await target_select.manual_select_target(page, hide_groups=True) 33 | 34 | # opens status file of the target person 35 | status_file: str = open(tracking_folder_path / f'status_{target_name}.txt', 'w').close() 36 | status_file: str = open(tracking_folder_path / f'status_{target_name}.txt', 'a') 37 | 38 | # default assignes 39 | is_sound_enabled: bool = True 40 | last_status: str = 'offline' 41 | try: 42 | print(f'Tracking: {target_name}') 43 | __logger.info("Tracking target") 44 | status_file.write(f'Tracking: {target_name}\n') 45 | while True: 46 | status: str = await target_data.get_last_seen_from_focused_target(page) # checks last seen 47 | if status == 'online': # if last seen is online then shows online 48 | is_online: bool = True 49 | else: # if nothing is there so shows offline 50 | is_online: bool = False 51 | status: str = 'offline' 52 | # play a notification sound on online 53 | if last_status != is_online: 54 | if is_online: 55 | try: 56 | if is_sound_enabled: 57 | playsound('plucky.wav') 58 | except Exception as e: 59 | print("Error: Couldn't play the sound.") 60 | is_sound_enabled: bool = False 61 | print( 62 | f'{datetime.now().strftime("%d/%m/%Y, %H:%M:%S")}' + f' - Status: {status}' 63 | ) 64 | status_file.write( 65 | f'{datetime.now().strftime("%d/%m/%Y, %H:%M:%S")}' + f' - Status: {status}\n') 66 | last_status: str = is_online 67 | time.sleep(0.5) 68 | except KeyboardInterrupt: 69 | __logger.error("User Pressed Ctrl+C") 70 | finally: 71 | # save the status and close the file 72 | status_file.close() 73 | print(f'\nStatus file saved in: {str(tracking_folder_path/"status_")}{target_name}.txt') 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rohitpotter12@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /wplay/chatbot.py: -------------------------------------------------------------------------------- 1 | from googlesearch import search 2 | from pyppeteer import launch 3 | from wplay.utils.helpers import chatbot_image_folder_path 4 | 5 | 6 | async def Bot(last_Message): 7 | """ 8 | Function to perform instruction as instructed to bot. 9 | """ 10 | print('\n Bot activated') 11 | first_last_Message = "".join(last_Message.split()) 12 | simple_menu = { 13 | "hi": say_hi, 14 | "help": _help_commands, 15 | "goodmorning": say_goodmorning, 16 | "goodnight": say_goodnight, 17 | "howareyou?": say_fine, 18 | } 19 | simple_menu_keys = simple_menu.keys() 20 | result = [] 21 | 22 | try: 23 | command_args = first_last_Message[1:].split(" ", 1) 24 | command_arg = last_Message[1:].split(" ", 1) 25 | 26 | if len(command_args) == 1 and command_args[0] in simple_menu_keys: 27 | return simple_menu[command_args[0]]() 28 | 29 | elif command_arg[0] == 'google': 30 | query = "".join(command_arg[1]) 31 | for j in search(query, tld="co.in", num=10, stop=10, pause=2): 32 | result.append(j) 33 | print("Sending links for query") 34 | return result 35 | 36 | elif command_arg[0] == "image": 37 | query = "".join(command_arg[1]) 38 | await takeScreenshot(query) 39 | print("Taking screenshot of google image for query") 40 | return "Sending you screenshot" 41 | 42 | elif command_arg[0] == "maps": 43 | query = "".join(command_arg[1]) 44 | map_parameters_list = query.replace(" ", "") 45 | map_parameters = map_parameters_list.split(',') 46 | base_url = "https://www.google.com/maps/dir/?api=1&" 47 | custom_url = base_url + "origin={ori}&destination={dest}&travelmode={t_mode}".format(ori=map_parameters[0], dest=map_parameters[1], t_mode=map_parameters[2]) 48 | print("Sending link for google maps") 49 | return custom_url 50 | 51 | else: 52 | return "Wrong command. Send me /help to see a list of valid commands" 53 | 54 | except KeyError as e: 55 | print("Key Error Exception: {err}".format(err=str(e))) 56 | 57 | 58 | def say_hi(): 59 | print("Saying hi") 60 | return "Wplay chatbot says hi! Hope you are having a nice day..." 61 | 62 | 63 | def say_goodmorning(): 64 | print("Saying good morning") 65 | return "Bot says Good Morning! Have a Good Day..." 66 | 67 | 68 | def say_goodnight(): 69 | print("Saying good night") 70 | return "Bot says Good Night! Sweet Dreams..." 71 | 72 | 73 | def say_fine(): 74 | print("Saying I am Fine!") 75 | return "Bot says I am Fine Thank You! How are you?" 76 | 77 | 78 | def _help_commands(): 79 | print("Asking for help") 80 | return "How may I assist you with help\n"\ 81 | "List of commands:\n" \ 82 | "/hi (bot says hi), " \ 83 | "/all_commands (ist of all commands), " \ 84 | "/good morning, " \ 85 | "/good night, " \ 86 | "/how are you? " \ 87 | "/google {query} " \ 88 | "/image {query} " \ 89 | "/maps {origin}, {destination}, {mode:driving/bicycling/transit/two-wheeler/walking}" 90 | 91 | 92 | async def takeScreenshot(qry): 93 | browser = await launch() 94 | page = await browser.newPage() 95 | await page.goto('https://www.google.com/search?q={}&source=lnms&tbm=isch'.format(qry)) 96 | image_path = str(chatbot_image_folder_path / '{}.png'.format(qry)) 97 | await page.screenshot({'path': image_path}) 98 | await browser.close() 99 | -------------------------------------------------------------------------------- /wplay/message_service.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | import threading 4 | import time 5 | import json 6 | 7 | from wplay.utils import browser_config 8 | from wplay.utils.target_search import search_target_by_number 9 | from wplay.utils import target_select 10 | from wplay.utils import io 11 | from wplay.utils import helpers 12 | from wplay.utils import verify_internet 13 | from wplay.utils.Logger import Logger 14 | from wplay.utils.MessageStack import MessageStack 15 | # endregion 16 | 17 | 18 | # region LOGGER 19 | import logging 20 | __logger = Logger(Path(__file__).name, logging.DEBUG) 21 | # endregion 22 | 23 | 24 | """ 25 | Messages file structure, your program should add messages this way 26 | inside the file "messages.json" located at user/wplay/messagesJSON folder. 27 | 28 | { 29 | "messages": [ 30 | { 31 | "uuid": "33bf7c667f8011ea96971c3947562893", 32 | "number": "5562999999999", 33 | "message": "*Bold Hello World*" 34 | }, 35 | { 36 | "uuid": "46ca6d284f8058ee89354e2987862869", 37 | "number": "5562888888888", 38 | "message": ["Hello!!!","Multi-line"] 39 | } 40 | ] 41 | } 42 | """ 43 | 44 | 45 | async def message_service(): 46 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 47 | __logger.info("Message Service On.") 48 | print("Message Service is ON, press CTRL+C to stop.") 49 | print("Listening for messages in file 'messages.json' inside user/wplay/messagesJSON folder.") 50 | # Initialize a instance of MessageStack 51 | message_stack = MessageStack() 52 | # Move all messages from open_messages.json to messages.json when the program starts 53 | message_stack.move_all_messages(helpers.open_messages_json_path, helpers.messages_json_path) 54 | 55 | while True: 56 | if verify_internet.internet_avalaible(): 57 | try: 58 | # Try to get the message 59 | current_msg = next(message_stack.get_message()) 60 | 61 | # Move message from messages.json to open_messages.json 62 | message_stack.move_message(helpers.messages_json_path, helpers.open_messages_json_path, current_msg['uuid']) 63 | 64 | try: 65 | if await search_target_by_number(page, current_msg['number']): 66 | await io.send_message(page, current_msg['message']) 67 | message_stack.remove_message(current_msg['uuid'], helpers.open_messages_json_path) 68 | except ValueError: 69 | __logger.debug("Wrong JSON Formatting. Message Deleted.") 70 | message_stack.remove_message(current_msg['uuid'], helpers.open_messages_json_path) 71 | except Exception as e: 72 | # If any error occurs that is not because of wrong data, 73 | # the message will be moved back to messages.json 74 | __logger.error(f'Error handling and sending the message: {str(e)}') 75 | MessageStack().move_message(helpers.open_messages_json_path, helpers.messages_json_path, current_msg['uuid']) 76 | except (StopIteration, json.JSONDecodeError): 77 | # if there are no messages to catch we will have this 'Warning', will try again after a time 78 | time.sleep(1) 79 | else: 80 | __logger.debug('Internet is not available, trying again after 15 seconds.') 81 | time.sleep(15) 82 | 83 | # Move messages from open_messages.json to messages.json that wasn't sended. 84 | message_stack.move_all_messages(helpers.open_messages_json_path, helpers.messages_json_path) 85 | -------------------------------------------------------------------------------- /wplay/target_info.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from wplay.utils import browser_config 3 | from wplay.utils import target_search 4 | from wplay.utils import target_select 5 | import phonenumbers 6 | from phonenumbers import carrier 7 | from phonenumbers import geocoder 8 | from phonenumbers import timezone 9 | import re 10 | import sys 11 | from pathlib import Path 12 | from wplay.utils.Logger import Logger 13 | # end IMPORTS 14 | 15 | 16 | # region LOGGER 17 | __logger = Logger(Path(__file__).name) 18 | # endregion 19 | 20 | 21 | def formatNumber(InputNumber): 22 | return re.sub(r"(?:\+)?(?:[^[0-9]*)", "", InputNumber) 23 | 24 | 25 | def localScan(InputNumber, print_results=True): 26 | print("Running local scan...") 27 | 28 | FormattedPhoneNumber = "+" + formatNumber(InputNumber) 29 | 30 | try: 31 | PhoneNumberObject = phonenumbers.parse(FormattedPhoneNumber, None) 32 | except Exception as e: 33 | print(e) 34 | else: 35 | if not phonenumbers.is_valid_number(PhoneNumberObject): 36 | return False 37 | 38 | number = phonenumbers.format_number(PhoneNumberObject, phonenumbers.PhoneNumberFormat.E164).replace("+", "") 39 | numberCountryCode = phonenumbers.format_number(PhoneNumberObject, phonenumbers.PhoneNumberFormat.INTERNATIONAL).split(" ")[0] 40 | numberCountry = phonenumbers.region_code_for_country_code(int(numberCountryCode)) 41 | 42 | localNumber = phonenumbers.format_number(PhoneNumberObject, phonenumbers.PhoneNumberFormat.E164).replace(numberCountryCode, "") 43 | internationalNumber = phonenumbers.format_number(PhoneNumberObject, phonenumbers.PhoneNumberFormat.INTERNATIONAL) 44 | 45 | country = geocoder.country_name_for_number(PhoneNumberObject, "en") 46 | location = geocoder.description_for_number(PhoneNumberObject, "en") 47 | carrierName = carrier.name_for_number(PhoneNumberObject, "en") 48 | 49 | if print_results: 50 | print("International format: {}".format(internationalNumber)) 51 | print("Local format: {}".format(localNumber)) 52 | print("Country found: {} ({})".format(country, numberCountryCode)) 53 | print("City/Area: {}".format(location)) 54 | print("Carrier: {}".format(carrierName)) 55 | for timezoneResult in timezone.time_zones_for_number(PhoneNumberObject): 56 | print("Timezone: {}".format(timezoneResult)) 57 | 58 | if phonenumbers.is_possible_number(PhoneNumberObject): 59 | print("The number is valid and possible.") 60 | else: 61 | print("The number is valid but might not be possible.") 62 | 63 | numberObj = {} 64 | numberObj["input"] = InputNumber 65 | numberObj["default"] = number 66 | numberObj["local"] = localNumber 67 | numberObj["international"] = internationalNumber 68 | numberObj["country"] = country 69 | numberObj["countryCode"] = numberCountryCode 70 | numberObj["countryIsoCode"] = numberCountry 71 | numberObj["location"] = location 72 | numberObj["carrier"] = carrierName 73 | 74 | return numberObj 75 | 76 | 77 | def scanNumber(InputNumber): 78 | print("[!] ---- Fetching informations for {} ---- [!]".format(formatNumber(InputNumber))) 79 | number = localScan(InputNumber) 80 | 81 | if not number: 82 | print(("Error: number {} is not valid. Skipping.".format(formatNumber(InputNumber)))) 83 | sys.exit() 84 | 85 | print("Scan finished.") 86 | 87 | 88 | def target_contact_number(num): 89 | target_contact_number.phone_number = num 90 | 91 | 92 | async def target_info(target): 93 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 94 | 95 | if target is not None: 96 | try: 97 | await target_search.search_and_select_target(page, target) 98 | except Exception as e: 99 | print(e) 100 | await page.reload() 101 | await target_search.search_and_select_target_without_new_chat_button(page, target) 102 | else: 103 | await target_select.manual_select_target(page) 104 | """ 105 | # to find location by ip address 106 | print('Get you ipinfo token from https://ipinfo.io/account') 107 | ip_address = '*' 108 | token = str(input("Enter your ipinfo token: ")) 109 | ip_string = 'curl ipinfo.io/'+ip_address+'?token='+token+'' 110 | os.system(ip_string) 111 | """ 112 | __logger.info("Writing target's information") 113 | scanNumber(target_contact_number.phone_number) 114 | -------------------------------------------------------------------------------- /wplay/download_media.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | from pathlib import Path 3 | 4 | from wplay.utils import browser_config 5 | from wplay.utils import target_search 6 | from wplay.utils import target_select 7 | from wplay.utils.helpers import media_path 8 | from wplay.utils.Logger import Logger 9 | from wplay.utils.helpers import whatsapp_selectors_dict 10 | import time 11 | # endregion 12 | 13 | 14 | # region LOGGER 15 | __logger = Logger(Path(__file__).name) 16 | # endregion 17 | 18 | 19 | async def download_media(target): 20 | page, browser = await browser_config.configure_browser_and_load_whatsapp() 21 | 22 | if target is not None: 23 | try: 24 | await target_search.search_and_select_target(page, target) 25 | except Exception as e: 26 | print(e) 27 | await page.reload() 28 | await target_search.search_and_select_target_without_new_chat_button( 29 | page, 30 | target 31 | ) 32 | else: 33 | await target_select.manual_select_target(page) 34 | 35 | count = int(input("Count of media you want to download: ")) 36 | 37 | # Click on the photo element of the target 38 | await page.waitForSelector(whatsapp_selectors_dict['target_name_selector'], visible=True) 39 | await page.evaluate(f'''document.querySelector('{whatsapp_selectors_dict['target_name_selector']}').click()''') 40 | 41 | time.sleep(1) 42 | 43 | # Click on the `Media, Link and Docs` text 44 | await page.waitForSelector(whatsapp_selectors_dict['media_text']) 45 | await page.click(whatsapp_selectors_dict['media_text']) 46 | 47 | # Click on the most recent media element 48 | while True: 49 | try: 50 | await page.evaluate(f'''document.querySelector('{whatsapp_selectors_dict['media_images']}').click()''') 51 | break 52 | except Exception as e: 53 | print("", end='') 54 | 55 | media_arr = {'img': [], 'vid': []} 56 | 57 | # Currently downloads the last 50 medias 58 | for _ in range(count): 59 | try: 60 | try: 61 | # If media is an image 62 | countTry = 0 # Threshold of how many times to try looking for media 63 | while True: 64 | if countTry > 500: 65 | await page.waitForSelector(whatsapp_selectors_dict['left_arrow_button']) 66 | 67 | img = await page.evaluate(f'''() => [...document.querySelectorAll('{whatsapp_selectors_dict['media_url_img']}')] 68 | .map(element => element.src)''') 69 | if img and len(img) == 2: 70 | img = img[-1] 71 | if img not in media_arr: 72 | media_arr['img'].append(img) 73 | break 74 | countTry += 1 75 | time.sleep(0.3) 76 | except Exception as e: 77 | # If media is a video or gif 78 | countTry = 0 79 | while True: 80 | vid = await page.evaluate(f'''() => [...document.querySelectorAll('{whatsapp_selectors_dict['media_url_vid']}')] 81 | .map(element => element.src)''') 82 | if vid: 83 | vid = vid[-1] 84 | media_arr['vid'].append(vid) 85 | break 86 | 87 | # Go to next media element 88 | await page.waitForSelector(whatsapp_selectors_dict['left_arrow_button']) 89 | await page.evaluate(f'''document.querySelector('{whatsapp_selectors_dict['left_arrow_button']}').click()''') 90 | time.sleep(0.5) 91 | 92 | except Exception as e: 93 | print(e) 94 | 95 | count = 0 96 | newPage = await browser.newPage() 97 | # Downloading media 98 | for image in media_arr['img']: 99 | try: 100 | count += 1 101 | print(image) 102 | viewSource = await newPage.goto(image) 103 | f = open(media_path / f'{count}.jpg', 'wb') 104 | f.write(await viewSource.buffer()) 105 | f.close() 106 | except Exception as e: 107 | print("Error saving image", e) 108 | 109 | for video in media_arr['vid']: 110 | try: 111 | count += 1 112 | viewSource = await newPage.goto(video) 113 | f = open(media_path / f'{count}.mp4', 'wb') 114 | f.write(await viewSource.buffer()) 115 | f.close() 116 | except Exception as e: 117 | print("Error saving video", e) 118 | 119 | print("Saved the media to the 'media' dir") 120 | time.sleep(10) 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | # whatsapp-play 5 | 6 | [![Downloads](https://pepy.tech/badge/wplay)](https://pepy.tech/project/wplay) 7 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/749acf4cad424fbeb96a412963aa83ea)](https://app.codacy.com/app/rpotter12/whatsapp-play?utm_source=github.com&utm_medium=referral&utm_content=rpotter12/whatsapp-play&utm_campaign=Badge_Grade_Settings) 8 | [![PyPi](https://img.shields.io/pypi/v/wplay)](https://pypi.org/project/wplay/) 9 | ![CircleCI](https://circleci.com/gh/rpotter12/whatsapp-play/tree/master.svg?style=svg&circle-token=2b67dd21e60a01fdd36a670629574479aeb2f5c4) 10 | [![Docker](https://img.shields.io/docker/cloud/build/rpotter12/whatsapp-play)](https://hub.docker.com/repository/docker/rpotter12/whatsapp-play/general) 11 | [![Build Status](https://travis-ci.org/rpotter12/whatsapp-play.svg?branch=master)](https://travis-ci.org/rpotter12/whatsapp-play) 12 | [![codecov](https://codecov.io/gh/rpotter12/whatsapp-play/branch/master/graph/badge.svg)](https://codecov.io/gh/rpotter12/whatsapp-play) 13 | [![twitter](https://img.shields.io/twitter/url/https/github.com/rpotter12/whatsapp-play.svg?style=social)](https://twitter.com/rpotter121998) 14 | [![HitCount](http://hits.dwyl.io/rpotter12/whatsapp-play.svg)](http://hits.dwyl.io/rpotter12/whatsapp-play) 15 | [![Gitter](https://badges.gitter.im/whatsapp-play/community.svg)](https://gitter.im/whatsapp-play/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 16 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/rpotter12/whatsapp-play) 17 | 18 |
19 | 20 | 21 | A command line software through which you can play with your WhatsApp. The software aims to provide all the facilities to use and implement a multitude of WhatsApp features. Blog link: [https://github.com/rpotter12/rpotter12.github.io/blob/master/blogs/blog3-tracking-26-07-2019.md](https://github.com/rpotter12/rpotter12.github.io/blob/master/blogs/blog3-tracking-26-07-2019.md) 22 | 23 | ## Features 24 | - Online/offline Tracker 25 | - Terminal Chat 26 | - Chat Intermediator 27 | - Message Service 28 | - Telegram Bot 29 | - Message Blast 30 | - Message Timer 31 | - Save Chat 32 | - Schedule Message 33 | - About Changer 34 | - Get News (Get API from and paste it in newsapi = NewsApiClient(api_key="YOUR API KEY")) 35 | - Get Profile Photos 36 | - Broadcast Message 37 | - Target Info (Profile Info) 38 | - Download Media 39 | 40 | --- 41 | 42 | ## Installation 43 | 44 | ### Install whatsapp-play from PyPI:
45 | Windows: `python -m pip install wplay`
46 | Unix/Linux/Mac: `python3 -m pip install wplay`
47 | **Installation Video:** [Simple Installation Link](https://youtu.be/HS6ksu6rCxQ) 48 | 49 | ### Alternate way - Run whatsapp-play from source code:
50 | **Windows**
51 | ``` 52 | $ git clone https://github.com/rpotter12/whatsapp-play.git 53 | $ cd whatsapp-play 54 | $ python -m pip install -r requirements.txt 55 | $ python -m wplay -h 56 | ``` 57 | 58 | **Unix/Linux/Mac**
59 | ``` 60 | $ git clone https://github.com/rpotter12/whatsapp-play.git 61 | $ cd whatsapp-play 62 | $ python3 -m pip install -r requirements.txt 63 | $ python3 -m wplay -h 64 | ``` 65 | 66 | ## Usage 67 |
68 | 69 | For detailed usage of command visit: [https://github.com/rpotter12/whatsapp-play/wiki/Usage](https://github.com/rpotter12/whatsapp-play/wiki/Usage) 70 | 71 | ## Contribute 72 | 73 | The easiest way to contribute to **Whatsapp-Play** is by starring the repository and opening more and more [issues](https://github.com/rpotter12/whatsapp-play/issues) for features you'd like to see in future.
74 | 75 | First step is to create a fork and clone, then you can solve the [issues](https://github.com/rpotter12/whatsapp-play/issues) listed and help us find new ones. Then try debugging with Visual Studio Code it is necessary to create a launcher with the arguments.
76 | 77 | Steps to create a launcher with arguments follow the steps bellow:
78 | 1. Click in 'Debug' tab 79 | 1. Click in 'Add Configuration' 80 | 1. Select 'Module' 81 | 1. Type 'wplay' and press Enter 82 | 1. A json file will be opened. Inside configurations add the args, for example: "args":["-wb","name"] 83 | 84 | **Debug Tutorial Video:** [Debug Tutorial Link](https://youtu.be/NyJgUGvyWnY)
85 | Check more about contribution guidelines [here](https://www.github.com/rpotter12/whatsapp-play/CONTRIBUTION.md) 86 | 87 | ## Disclaimer 88 | This software is for educational purpose only. Keeping eye on a innocent person can make person's life stressful. 89 | 90 | ## License 91 | [![License](https://img.shields.io/github/license/rpotter12/whatsapp-play.svg)](https://github.com/rpotter12/whatsapp-play/blob/master/README.md) 92 | 93 | ***If you like the project, support us by star*** 94 | -------------------------------------------------------------------------------- /wplay/utils/MessageStack.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | from typing import List, Iterator 4 | import json 5 | 6 | from wplay.utils import helpers 7 | from wplay.utils.Logger import Logger 8 | # endregion 9 | 10 | 11 | class MessageStack(): 12 | def __init__(self): 13 | self.logger = Logger(Path(__file__).name) 14 | self.__create_json_file(helpers.messages_json_path) 15 | self.__ensure_valid_json(helpers.messages_json_path) 16 | self.__create_json_file(helpers.open_messages_json_path) 17 | self.__ensure_valid_json(helpers.open_messages_json_path) 18 | 19 | def __create_json_file(self, file_path: Path): 20 | if not file_path.is_file(): 21 | open(file_path, 'w').close() 22 | self.logger.info(f'{file_path.name} created.') 23 | 24 | def __write_json(self, data: dict, file_path: Path): 25 | with open(file_path, "w") as json_file: 26 | json.dump(data, json_file, indent=4) 27 | 28 | def __ensure_valid_json(self, file_path: Path): 29 | valid_data = {"messages": list()} 30 | try: 31 | with open(file_path) as json_file: 32 | data = json.load(json_file) 33 | if 'messages' not in data: 34 | self.__write_json(valid_data, file_path) 35 | except json.JSONDecodeError: 36 | # Empty or Invalid Json 37 | self.__write_json(valid_data, file_path) 38 | 39 | def append_message(self, message: dict, file_path: Path): 40 | """ 41 | Append messages into json file. 42 | 43 | Arguments: 44 | message -- dict with a message 45 | file_path {Path} -- open_messages_json_path or messages_json_path from helpers 46 | """ 47 | self.__ensure_valid_json(file_path) 48 | 49 | with open(file_path) as json_file: 50 | json_data = json.load(json_file) 51 | json_data['messages'].append(message) 52 | 53 | self.__write_json(json_data, file_path) 54 | self.logger.info(f'Message appended to {file_path.name}') 55 | 56 | def get_message( 57 | self, 58 | from_file_path: Path = helpers.messages_json_path) -> Iterator[dict]: 59 | """ 60 | Yield a message from a file. 61 | 62 | Arguments: 63 | from_file_path {Path} -- open_messages_path or messages_path from helpers 64 | 65 | Exception: 66 | raise StopIteration, json.JSONDecodeError, KeyError if file is empty, or the iteration stopped or the key isn't finded. 67 | 68 | Yields: 69 | [dict] -- Yield a dict with all message data 70 | """ 71 | with open(from_file_path) as json_file: 72 | data = json.load(json_file) 73 | for message in data['messages']: 74 | yield message 75 | 76 | def get_all_messages( 77 | self, 78 | from_file_path: Path = helpers.messages_json_path) -> List[dict]: 79 | self.__ensure_valid_json(from_file_path) 80 | with open(from_file_path) as json_file: 81 | data = json.load(json_file) 82 | return data['messages'] 83 | 84 | def move_message(self, from_file_path: Path, to_file_path: Path, uuid: str): 85 | with open(from_file_path) as json_file: 86 | data = json.load(json_file) 87 | for message in data['messages']: 88 | if uuid in message['uuid']: 89 | self.append_message(message, to_file_path) 90 | self.remove_message(uuid, from_file_path) 91 | 92 | def move_all_messages(self, from_file_path: Path, to_file_path: Path): 93 | self.__ensure_valid_json(from_file_path) 94 | with open(from_file_path) as json_file: 95 | data = json.load(json_file) 96 | for message in data['messages']: 97 | self.append_message(message, to_file_path) 98 | self.remove_message(message['uuid'], from_file_path) 99 | 100 | def remove_message(self, uuid: str, file_path: Path): 101 | with open(file_path) as json_file: 102 | data = json.load(json_file) 103 | for i, message in enumerate(data['messages']): 104 | if uuid in message['uuid']: 105 | del data['messages'][i] 106 | self.__write_json(data, file_path) 107 | self.logger.info(f"Message Deleted Successfully from {file_path.name}") 108 | 109 | def remove_all_messages(self, file_path: Path): 110 | with open(file_path) as json_file: 111 | data = json.load(json_file) 112 | for i, message in enumerate(data['messages']): 113 | del data['messages'][i] 114 | self.__write_json(data, file_path) 115 | self.logger.info(f"Deleted Successfully all messages from {file_path.name}") 116 | -------------------------------------------------------------------------------- /tests/test_func.py: -------------------------------------------------------------------------------- 1 | #region imports 2 | import functools 3 | import os 4 | import shutil 5 | import stat 6 | import subprocess 7 | import sys 8 | import time 9 | 10 | import psutil 11 | from psutil import MACOS 12 | from psutil import POSIX 13 | from psutil import WINDOWS 14 | 15 | from psutil._compat import which 16 | #endregion 17 | 18 | 19 | __all__ = [ 20 | # constants 21 | 'DEVNULL' , 'PYTHON_EXE', 'TESTFILE_PREFIX' , 'TESTFN', 22 | # subprocesses 23 | 'get_test_subprocess', 24 | # fs utils 25 | 'safe_rmpath' , 26 | # sync primitives 27 | 'wait_for_pid', 'wait_for_file', 28 | ] 29 | 30 | 31 | TESTFILE_PREFIX = '$testfn' 32 | if os.name == 'java': 33 | # Jython disallows @ in module names 34 | TESTFILE_PREFIX = '$psutil-test-' 35 | else: 36 | TESTFILE_PREFIX = '@psutil-test-' 37 | TESTFN = os.path.join(os.path.realpath(os.getcwd()), TESTFILE_PREFIX) 38 | TESTFN = TESTFN + str(os.getpid()) 39 | 40 | _TESTFN = TESTFN + '-internal' 41 | 42 | 43 | def _get_py_exe(): 44 | def attempt(exe): 45 | try: 46 | subprocess.check_call( 47 | [exe, "-V"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 48 | except Exception: 49 | return None 50 | else: 51 | return exe 52 | 53 | if MACOS: 54 | exe = \ 55 | attempt(sys.executable) or \ 56 | attempt(os.path.realpath(sys.executable)) or \ 57 | attempt(which("python%s.%s" % sys.version_info[:2])) or \ 58 | attempt(psutil.Process().exe()) 59 | if not exe: 60 | raise ValueError("can't find python exe real abspath") 61 | return exe 62 | else: 63 | exe = os.path.realpath(sys.executable) 64 | assert os.path.exists(exe), exe 65 | return exe 66 | 67 | 68 | PYTHON_EXE = _get_py_exe() 69 | DEVNULL = open(os.devnull, 'r+') 70 | 71 | _subprocesses_started = set() 72 | 73 | 74 | def get_test_subprocess(cmd=None, **kwds): 75 | """Creates a python subprocess which does nothing for 30 secs and 76 | return it as subprocess.Popen instance. 77 | If "cmd" is specified that is used instead of python. 78 | By default stdin and stdout are redirected to /dev/null. 79 | It also attemps to make sure the process is in a reasonably 80 | initialized state. 81 | The process is registered for cleanup on reap_children(). 82 | """ 83 | kwds.setdefault("stdin", DEVNULL) 84 | kwds.setdefault("stdout", DEVNULL) 85 | kwds.setdefault("cwd", os.getcwd()) 86 | kwds.setdefault("env", os.environ) 87 | if WINDOWS: 88 | # Prevents the subprocess to open error dialogs. This will also 89 | # cause stderr to be suppressed, which is suboptimal in order 90 | # to debug broken tests. 91 | CREATE_NO_WINDOW = 0x8000000 92 | kwds.setdefault("creationflags", CREATE_NO_WINDOW) 93 | if cmd is None: 94 | safe_rmpath(_TESTFN) 95 | pyline = "from time import sleep;" \ 96 | "open(r'%s', 'w').close();" \ 97 | "sleep(30);" % _TESTFN 98 | cmd = [PYTHON_EXE, "-c", pyline] 99 | sproc = subprocess.Popen(cmd, **kwds) 100 | _subprocesses_started.add(sproc) 101 | #wait_for_file(_TESTFN, delete=True, empty=True) 102 | else: 103 | sproc = subprocess.Popen(cmd, **kwds) 104 | _subprocesses_started.add(sproc) 105 | wait_for_pid(sproc.pid) 106 | return sproc 107 | 108 | 109 | def wait_for_pid(pid): 110 | """Wait for pid to show up in the process list then return. 111 | Used in the test suite to give time the sub process to initialize. 112 | """ 113 | psutil.Process(pid) 114 | if WINDOWS: 115 | # give it some more time to allow better initialization 116 | time.sleep(0.01) 117 | 118 | 119 | def wait_for_file(fname, delete=True, empty=False): 120 | """Wait for a file to be written on disk with some content.""" 121 | with open(fname, "rb") as f: 122 | data = f.read() 123 | if not empty: 124 | assert data 125 | if delete: 126 | safe_rmpath(fname) 127 | return data 128 | 129 | 130 | def safe_rmpath(path): 131 | "Convenience function for removing temporary test files or dirs" 132 | def retry_fun(fun): 133 | # On Windows it could happen that the file or directory has 134 | # open handles or references preventing the delete operation 135 | # to succeed immediately, so we retry for a while. See: 136 | # https://bugs.python.org/issue33240 137 | stop_at = time.time() + 1 138 | while time.time() < stop_at: 139 | try: 140 | return fun() 141 | except FileNotFoundError: 142 | pass 143 | 144 | try: 145 | st = os.stat(path) 146 | if stat.S_ISDIR(st.st_mode): 147 | fun = functools.partial(shutil.rmtree, path) 148 | else: 149 | fun = functools.partial(os.remove, path) 150 | if POSIX: 151 | fun() 152 | else: 153 | retry_fun(fun) 154 | except FileNotFoundError: 155 | pass -------------------------------------------------------------------------------- /wplay/utils/browser_config.py: -------------------------------------------------------------------------------- 1 | # region TUTORIAL 2 | ''' 3 | Go to region 'FOR SCRIPTING' and use the methods in your script! 4 | 5 | EXAMPLE OF USAGE: 6 | from wplay.pyppeteerUtils import pyppeteerConfig as pypConfig 7 | from wplay.pyppeteerUtils import pyppeteerSearch as pypSearch 8 | 9 | async def my_script(target): 10 | pages, browser = wait pyp.configure_browser_and_load_whatsapp( 11 | pypConfig.websites['whatsapp'] 12 | ) 13 | await pypSearch.search_for_target_and_get_ready_for_conversation( 14 | pages[0], 15 | target 16 | ) 17 | 18 | message = pypSearch.ask_user_for_message_breakline_mode() 19 | await pypSearch.send_message(pages[0], message) 20 | 21 | message2 = pypSearch.ask_user_for_message() 22 | await pypSearch.send_message(pages[0], message2) 23 | 24 | ''' 25 | # endregion 26 | 27 | 28 | # region IMPORTS 29 | from typing import Any, List 30 | from pathlib import Path 31 | 32 | import websockets.client 33 | from pyppeteer import launch, connection, launcher 34 | from pyppeteer.browser import Browser 35 | from pyppeteer.page import Page 36 | 37 | from wplay.utils.SessionManager import SessionManager 38 | from wplay.utils.helpers import websites, user_data_folder_path 39 | from wplay.utils.Logger import Logger 40 | # endregion 41 | 42 | 43 | # region LOGGER 44 | __logger = Logger(Path(__file__).name) 45 | # endregion 46 | 47 | 48 | # region FOR SCRIPTING 49 | async def configure_browser_and_load_whatsapp() -> (Page, Browser): 50 | """ 51 | Configure browser, configure the first page and open whatsapp website. 52 | 53 | Returns: 54 | Page -- return the first page, with whatsapp open 55 | Browser -- return the browser object 56 | """ 57 | __patch_pyppeteer() 58 | username, save_session = SessionManager.session_manager() 59 | browser = await __config_browser(username, save_session) 60 | pages = await get_pages(browser) 61 | first_page = pages[0] 62 | await config_page(first_page) 63 | await load_website(first_page, websites['whatsapp']) 64 | return first_page, browser 65 | 66 | 67 | async def get_pages(browser: Browser) -> List[Page]: 68 | __logger.debug('Getting open pages') 69 | return await browser.pages() 70 | 71 | 72 | async def open_new_page(browser: Browser): 73 | """ 74 | Open a new tab. 75 | """ 76 | __logger.debug('Opening new page(tab)') 77 | await browser.newPage() 78 | 79 | 80 | async def config_page(page: Page): 81 | __logger.debug('Configuring page') 82 | await __set_user_agent(page) 83 | # await __set_view_port(page) 84 | 85 | 86 | async def load_website(page: Page, website: str): 87 | __logger.debug(f'Loading website: {website}') 88 | await page.bringToFront() 89 | await page.goto(website, waitUntil='networkidle2', timeout=0) 90 | 91 | 92 | def exit_if_wrong_url(page: Page, browser: Browser, url_to_check: str): 93 | if not page.url == url_to_check: 94 | __logger.error('Exit due to Wrong URL!') 95 | browser.close() 96 | exit() 97 | # endregion 98 | 99 | 100 | # region PYPPETEER PATCH 101 | # https://github.com/miyakogi/pyppeteer/pull/160 102 | # HACK: We need this until this PR is accepted. Solves the bug bellow. 103 | # BUG:(Pyppeteer) The communication with Chromium are disconnected after 20s. 104 | def __patch_pyppeteer(): 105 | __logger.debug("Patching Pyppeteer.") 106 | 107 | class PatchedConnection(connection.Connection): 108 | def __init__(self, *args: Any, **kwargs: Any) -> None: 109 | super().__init__(*args, **kwargs) 110 | self._ws = websockets.client.connect( 111 | self._url, 112 | loop=self._loop, 113 | max_size=None, 114 | ping_interval=None, 115 | ping_timeout=None, 116 | ) 117 | 118 | connection.Connection = PatchedConnection 119 | launcher.Connection = PatchedConnection 120 | # endregion 121 | 122 | 123 | # region PYPPETEER PRIVATE FUNCTIONS 124 | async def __config_browser(username: str = None, save_session: bool = False): 125 | __logger.debug('Configuring Browser.') 126 | if username is not None and username.strip() != '' and save_session: 127 | return await launch(headless=False, autoClose=False, userDataDir=user_data_folder_path / username) 128 | else: 129 | return await launch(headless=False, autoClose=False) 130 | 131 | 132 | async def __set_user_agent(page: Page): 133 | await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36') 134 | 135 | 136 | async def __set_view_port(page: Page): 137 | await page.setViewport({'width': 1280, 'height': 800}) 138 | # endregion 139 | 140 | 141 | # region CODE THAT MIGHT BE USEFUL SOMEDAY 142 | ''' 143 | # FIX: 144 | # To load websites faster 145 | async def intercept(request, page_one, page_two): 146 | await page_one.setRequestInterception(True) 147 | await page_two.setRequestInterception(True) 148 | if any(request.resourceType == _ for _ in ('stylesheet', 'image', 'font', 'media')): 149 | await request.abort() 150 | else: 151 | await request.continue_() 152 | page.on('request', lambda req: asyncio.ensure_future(intercept(req))) 153 | ''' 154 | # endregion 155 | -------------------------------------------------------------------------------- /wplay/terminal_chat.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | from wplay.utils import browser_config 5 | from wplay.utils import target_search 6 | from wplay.utils import target_select 7 | from wplay.utils import io 8 | from wplay.chat_intermediator import intermediary 9 | from wplay import text_to_speech 10 | from wplay.utils.Logger import Logger 11 | from wplay.utils.helpers import logs_path 12 | from colorama import Fore, Style 13 | from wplay import chatbot 14 | # endregion 15 | 16 | 17 | # region LOGGER 18 | __logger = Logger(Path(__file__).name) 19 | # endregion 20 | 21 | 22 | async def chat(target): 23 | __logger.info("Chatting with target") 24 | page, _ = await browser_config.configure_browser_and_load_whatsapp() 25 | 26 | if target is not None: 27 | try: 28 | await target_search.search_and_select_target(page, target) 29 | except Exception as e: 30 | print(e) 31 | await page.reload() 32 | await target_search.search_and_select_target_without_new_chat_button(page, target) 33 | else: 34 | target = await target_select.manual_select_target(page) 35 | 36 | # selectors 37 | selector_values = "#main > div > div > div > div > div > div > div > div" 38 | selector_sender = "#main > div > div > div > div > div > div > div > div > div.copyable-text" 39 | 40 | # Getting all the messages of the chat 41 | try: 42 | __logger.info("Printing recent chat") 43 | await page.waitForSelector(selector_values) 44 | values = await page.evaluate(f'''() => [...document.querySelectorAll('{selector_values}')] 45 | .map(element => element.textContent)''') 46 | sender = await page.evaluate(f'''() => [...document.querySelectorAll('{selector_sender}')] 47 | .map(element => element.getAttribute("data-pre-plain-text"))''') 48 | new_values = [x[:-8] for x in values] 49 | new_list = [a + b for a, b in zip(sender, new_values)] 50 | final_list = new_list[-6:] 51 | for s in final_list: 52 | print("%s\n" % s) 53 | except Exception as e: 54 | print(e) 55 | 56 | print("\033[91m {}\033[00m".format("\nType '...' in a new line or alone in the message to change target person.\nType '#_FILE' to send Image/Video/Documentd etc.\nType '#_TTS' to convert text to speech and send audio file.\nType '#_FWD' to foward your last message received")) 57 | 58 | while True: 59 | await getMessages(page, target) 60 | message: list[str] = io.ask_user_for_message_breakline_mode() 61 | 62 | # Target Change 63 | if "..." in message: 64 | message.remove('...') 65 | await io.send_message(page, message) 66 | target = input("\n\nNew Target Name: ") 67 | if target is not None: 68 | await target_search.search_and_select_target(page, target) 69 | else: 70 | await target_select.manual_select_target(page) 71 | 72 | # Be an Intermediator 73 | if "#_FWD" in message: 74 | await target_search.search_and_select_target(page, intermediary.rec) 75 | await io.send_message(page, getMessages.foward_message) 76 | message = io.ask_user_for_message_breakline_mode() 77 | 78 | # Text to speech 79 | if "#_TTS" in message: 80 | await text_to_speech.text_to_speech(target) 81 | await io.send_file(page) 82 | 83 | # File Share: 84 | if "#_FILE" in message: 85 | message.remove("#_FILE") 86 | await io.send_file(page) 87 | 88 | await getMessages(page, target) 89 | await io.send_message(page, message) 90 | 91 | 92 | async def getMessages(page, target): 93 | """ 94 | Get the last messages of the chats. 95 | """ 96 | # selectors 97 | selector_values = "#main > div > div > div > div > div > div > div > div" 98 | selector_sender = "#main > div > div > div > div > div > div > div > div > div.copyable-text" 99 | try: 100 | # Getting all the messages of the chat 101 | await page.waitForSelector(selector_values) 102 | values = await page.evaluate(f'''() => [...document.querySelectorAll('{selector_values}')] 103 | .map(element => element.textContent)''') 104 | sender = await page.evaluate(f'''() => [...document.querySelectorAll('{selector_sender}')] 105 | .map(element => element.getAttribute("data-pre-plain-text"))''') 106 | lastMessage = values[-1] 107 | last_message_time = sender[-1].split(',') 108 | last_message_time = last_message_time[0].replace('[', '') 109 | lastMessage = lastMessage.replace(last_message_time, '') 110 | except Exception as e: 111 | print(e) 112 | lastMessage = "" 113 | lastOutgoingMessage = "" 114 | if lastOutgoingMessage != lastMessage: 115 | print(Fore.GREEN + f"{target}-", end="") 116 | print(lastMessage, end="") 117 | print(Style.RESET_ALL) 118 | getMessages.foward_message = lastMessage 119 | if "/image" in lastMessage: 120 | bot_msg = await chatbot.Bot(last_Message=lastMessage) 121 | await io.send_message(page, bot_msg) 122 | await io.send_file(page) 123 | elif lastMessage[0] == "/": 124 | bot_msg = await chatbot.Bot(last_Message=lastMessage) 125 | await io.send_message(page, bot_msg) 126 | lastOutgoingMessage = lastMessage 127 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | To start contributing to [whatsapp-play](https://github.com/rpotter12/whatsapp-play) project, please first discuss the change you wish to make via creating an issue 4 | in the issue section or any other method with the owners of this repository before making a change.
5 | We have a code of conduct, please follow it in all your interactions with the project.
6 | 7 | # Contributing to whatsapp-play 8 | 9 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1: 10 | 11 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 12 | 13 | - Discussing the current state of the code 14 | - [Reporting a bug]( https://github.com/rpotter12/whatsapp-play/blob/master/.github/IssueTemplate/BugReportTemplate.md) 15 | - [Submitting a fix](https://github.com/rpotter12/whatsapp-play/blob/master/.github/Pull_Request_Template.md) 16 | - [Proposing new features]( https://github.com/rpotter12/whatsapp-play/blob/master/.github/IssueTemplate/FeatureRequestTemplate.md) 17 | 18 | # Points to remember 19 | 20 | 1.Ensure any install or build dependencies are checked and verified before the end when releasing a build.
21 | 2.Update the README.md with details of changes including environment variables, various file parameters and container details **if needed**.
22 | 3.Try adding screenshots in your pull request of the changes you made.
23 | 4.You may be able to merge the pull request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the maintainer or reviewer to merge it for you. 24 | 25 | # Steps to follow :scroll: 26 | 27 | ### 1. Fork it :fork_and_knife: 28 | 29 | You can get your own fork/copy of [whatsapp-play]( https://github.com/rpotter12/whatsapp-play) by using the Fork button. 30 | 31 | [![Fork Button](https://help.github.com/assets/images/help/repository/fork_button.jpg)](https://github.com/rpotter12/whatsapp-play) 32 | 33 | ### 2. Clone it :busts_in_silhouette: 34 | 35 | You need to clone (download) it to local machine using 36 | 37 | ```sh 38 | git clone https://github.com/Your_Username/whatsapp-play.git 39 | ``` 40 | 41 | > This makes a local copy of repository in your machine. 42 | 43 | Once you have cloned the ` whatsapp-play ` repository in GitHub, move to that folder first using change directory command. 44 | 45 | ```sh 46 | # This will change directory to a folder whatsapp-play 47 | cd whatsapp-play 48 | ``` 49 | 50 | Move to this folder for all other commands. 51 | 52 | ### 3. Set it up :arrow_up: 53 | 54 | Run the following commands to see that *your local copy* has a reference to *your forked remote repository* in GitHub :octocat: 55 | 56 | ```sh 57 | git remote -v 58 | origin https://github.com/Your_Username/whatsapp-play.git (fetch) 59 | origin https://github.com/Your_Username/whatsapp-play.git (push) 60 | ``` 61 | 62 | Now, add a reference to the original [whatsapp-play](https://github.com/rpotter12/whatsapp-play) repository using 63 | 64 | ```sh 65 | git remote add upstream https://github.com/rpotter12/whatsapp-play.git 66 | ``` 67 | 68 | > This adds a new remote named ***upstream***. 69 | 70 | See the changes using 71 | 72 | ```sh 73 | git remote -v 74 | origin https://github.com/Your_Username/whatsapp-play.git (fetch) 75 | origin https://github.com/Your_Username/whatsapp-play.git (push) 76 | upstream https://github.com/rpotter12/whatsapp-play.git (fetch) 77 | upstream https://github.com/rpotter12/whatsapp-play.git (push) 78 | ``` 79 | 80 | ### 4. Sync it :recycle: 81 | 82 | Always keep your local copy of repository updated with the original repository. 83 | Before making any changes and/or in an appropriate interval, run the following commands *carefully* to update your local repository. 84 | 85 | ```sh 86 | # Fetch all remote repositories and delete any deleted remote branches 87 | git fetch --all --prune 88 | 89 | # Switch to `master` branch 90 | git checkout master 91 | 92 | # Reset local `master` branch to match `upstream` repository's `master` branch 93 | git reset --hard upstream/master 94 | 95 | # Push changes to your forked `whatsapp-play` repo 96 | git push origin master 97 | ``` 98 | 99 | ### 5. Ready Steady Go :turtle: :rabbit2: 100 | 101 | Once you have completed these steps, you are ready to start contributing by checking our `Help Wanted` Issues and creating [pull requests](https://github.com/rpotter12/whatsapp-play/pulls). 102 | 103 | ### 6. Create a new branch :bangbang: 104 | 105 | Whenever you are going to make contribution. Please create separate branch using command and keep your `master` branch clean (i.e. synced with remote branch). 106 | 107 | ```sh 108 | # It will create a new branch with name Branch_Name and will switch to that branch. 109 | git checkout -b Branch_Name 110 | ``` 111 | 112 | Create a separate branch for contribution and try to use same name of branch as of folder. 113 | 114 | To switch to desired branch 115 | 116 | ```sh 117 | # To switch from one folder to other 118 | git checkout Branch_Name 119 | ``` 120 | 121 | To add the changes to the branch. Use 122 | 123 | ```sh 124 | # To add all files to branch Branch_Name 125 | git add . 126 | ``` 127 | 128 | Type in a message relevant for the code reviewer using 129 | 130 | ```sh 131 | # This message gets associated with all files you have changed 132 | git commit -m 'relevant message' 133 | ``` 134 | 135 | Now, Push your awesome work to your remote repository using 136 | 137 | ```sh 138 | # To push your work to your remote repository 139 | git push -u origin Branch_Name 140 | ``` 141 | 142 | Finally, go to your repository in browser and click on `compare and pull requests`. 143 | Use our [pull request template format]( https://github.com/rpotter12/whatsapp-play/blob/master/.github/Pull_Request_Template.md) 144 | Then add a title and description to your pull request that explains your precious effort. 145 | 146 | Sit and relax till we review your PR, you've made your contribution to our project. 147 | 148 | :tada: :confetti_ball: :smiley: _**Happy Contributing**_ :smiley: :confetti_ball: :tada: 149 | -------------------------------------------------------------------------------- /wplay/utils/helpers.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | from pathlib import Path 3 | 4 | import signal 5 | import psutil 6 | from whaaaaat import style_from_dict, Token 7 | # endregion 8 | 9 | # region Whatsapp WEBSITES 10 | websites = {'whatsapp': 'https://web.whatsapp.com/', 'wpp_unknown': 'https://web.whatsapp.com/send?phone='} 11 | # endregion 12 | 13 | # region SELECTORS 14 | whatsapp_selectors_dict = { 15 | 'login_area': '#app > div > div > div.landing-header', 16 | 17 | 'new_chat_button': '#side > header div[role="button"] span[data-icon="chat"]', 18 | 'search_contact_input_new_chat': '#app > div > div > div > div > span > div > span > div > div > div > label > div > div', 19 | 'contact_list_elements_filtered_new_chat': '#app > div > div > div > div > span > div > span > div > div > div > div > div > div > div > div > div > div > div > span > span[title][dir]', 20 | 'group_list_elements_filtered_new_chat': '#app > div > div > div > div > span > div > span > div > div > div > div > div > div > div > div > div > div > div > div > span[title][dir]', 21 | 22 | 'search_contact_input': '#side > div > div > label > div > div', 23 | 'chat_list_elements_filtered': '#pane-side > div > div > div > div > div > div > div > div > div > span > span[title][dir]', 24 | 25 | 'target_focused_title': '#main > header div > div > span[title]', 26 | 'message_area': '#main > footer div.selectable-text[contenteditable]', 27 | 'last_seen': '#main > header > div > div > span[title]', 28 | 'target_chat_header': '#main > header', 29 | 'contact_info_page_elements': '#app > div > div > div:nth-child(2) > div:last-of-type > span > div > span > div > div > div:first-child', 30 | 'contact_info_page_group_element_heading': '#app > div > div > div:nth-child(2) > div:last-of-type > ' 31 | 'span > div > span > div > div:nth-child(5)>div>div>div>div:first-child>span', 32 | 'contact_info_page_group_elements': '#app > div > div > div:nth-child(2) > div:last-of-type > ' 33 | 'span > div > span > div > div:nth-child(5)>div:nth-child(2)>div>div', 34 | 'contact_info_page_close_button': '#app > div > div > div > div > span > div > span > div > header > div > div > button', 35 | 'chat_or_message_search': '#side > div:nth-child(3) > div > label > div > div:last-child', 36 | 'chats_groups_messages_elements': '#side > div:last-child > div > div > div > div', 37 | 'contact_element': 'span > span > span[class^="matched-text"]', 38 | 'group_element': 'div:last-child > div:first-child > div:first-child > div > span > span[class^="matched-text"]', 39 | 'attach_file': '#main > header > div > div > div:nth-child(2) > div', 40 | 'choose_file': '#main > header > div > div > div > span > div > div > ul > li:nth-child(3) > button', 41 | 'send_file': '#app > div > div > div > div > span > div > span > div > div > div > span > div > div > span', 42 | 'profile_photo_element': '#side > header > div > div > img', 43 | 'about_edit_button_element': '#app > div > div > div > div > span > div > div > div > div:nth-child(4) > div > div > span > div > span', 44 | 'about_text_area': '#app > div > div > div > div > span > div > div > div > div:nth-child(4) > div > div > div > div', 45 | 'contact_info_page_target_group_name_element': 'div:nth-child(2)>div>div> div:last-of-type', 46 | 'contact_info_page_target_group_creation_info_element': ':scope > div:last-child > span', 47 | 'contact_info_page_target_group_description_element': ':scope > div:last-child span:first-of-type', 48 | 'contact_info_page_target_group_member_elements': ':scope > div:nth-child(4) > div > div', 49 | 'invalid_number_ok_button': '#app > div > span> div > span > div > div > div > div > div > div > div', 50 | 'target_name_selector': "#main > header > div > div > div > span", 51 | 'media_text': "#app > div > div > div > div > span > div > span > div > div > div > div > div > div > div > div > span", 52 | 'media_images': "#app > div > div > div > div > span > div > span > div > div > span > div > div > div > div > div > div", 53 | 'left_arrow_button': "#app > div > span > div > div > div > div > div > span", 54 | 'media_url_img': "#app > div > span:nth-child(3) > div > div > div > div > div > div > div > div > img", 55 | 'media_url_vid': "#app > div > span:nth-child(3) > div > div > div > div > div > div > div > div > video", 56 | } 57 | # endregion 58 | 59 | # region PATHS 60 | data_folder_path = Path.home() / 'wplay' 61 | logs_path = data_folder_path / 'logs' 62 | log_file_path = logs_path / 'wplay.log' 63 | test_log_file_path = logs_path / 'testwplay.log' 64 | user_data_folder_path = data_folder_path / '.userData' 65 | profile_photos_path = data_folder_path / 'media' / 'profilePhotos' 66 | tracking_folder_path = data_folder_path / 'trackingData' 67 | messages_json_folder_path = data_folder_path / 'messagesJSON' / 'system' 68 | messages_json_path = data_folder_path / 'messagesJSON' / 'messages.json' 69 | open_messages_json_path = data_folder_path / 'messagesJSON' / 'system' / 'openMessages.json' 70 | media_path = data_folder_path / 'media' / 'media' 71 | save_chat_folder_path = data_folder_path / 'savedChats' 72 | audio_file_folder_path = data_folder_path / 'audioFiles' 73 | chatbot_image_folder_path = data_folder_path / 'ChatbotImage' 74 | # endregion 75 | 76 | # region MENU STYLES 77 | menu_style = style_from_dict({ 78 | Token.Separator: '#6C6C6C', 79 | Token.QuestionMark: '#FF9D00 bold', 80 | Token.Selected: '#5F819D', 81 | Token.Pointer: '#FF9D00 bold', 82 | Token.Instruction: '', # default 83 | Token.Answer: '#5F819D bold', 84 | Token.Question: '', 85 | }) 86 | # endregion 87 | 88 | 89 | # region FUNCTIONS 90 | def create_dirs(): 91 | logs_path.mkdir(parents=True, exist_ok=True) 92 | user_data_folder_path.mkdir(parents=True, exist_ok=True) 93 | profile_photos_path.mkdir(parents=True, exist_ok=True) 94 | tracking_folder_path.mkdir(parents=True, exist_ok=True) 95 | messages_json_folder_path.mkdir(parents=True, exist_ok=True) 96 | media_path.mkdir(parents=True, exist_ok=True) 97 | save_chat_folder_path.mkdir(parents = True, exist_ok = True) 98 | audio_file_folder_path.mkdir(parents = True, exist_ok = True) 99 | tracking_folder_path.mkdir(parents = True, exist_ok = True) 100 | messages_json_folder_path.mkdir(parents = True, exist_ok = True) 101 | chatbot_image_folder_path.mkdir(parents= True, exist_ok=True) 102 | 103 | 104 | def kill_child_processes(parent_pid, sig=signal.SIGTERM): 105 | try: 106 | parent = psutil.Process(parent_pid) 107 | except psutil.NoSuchProcess: 108 | return 109 | children = parent.children(recursive=True) 110 | print('Process Killed!') 111 | for process in children: 112 | process.send_signal(sig) 113 | # endregion 114 | -------------------------------------------------------------------------------- /wplay/__main__.py: -------------------------------------------------------------------------------- 1 | # region IMPORTS 2 | import argparse 3 | import asyncio 4 | import sys 5 | import os 6 | from pathlib import Path 7 | 8 | from pyfiglet import Figlet 9 | 10 | from wplay import online_tracker 11 | from wplay import message_blast 12 | from wplay import message_timer 13 | from wplay import terminal_chat 14 | from wplay import chat_intermediator 15 | from wplay import broadcast_message 16 | from wplay import save_chat 17 | from wplay import telegram_bot 18 | from wplay import schedule_message 19 | from wplay import about_changer 20 | from wplay import get_news 21 | from wplay import get_media 22 | from wplay import download_media 23 | from wplay import message_service 24 | from wplay import target_info 25 | from wplay import profile_download 26 | from wplay.utils.Logger import Logger 27 | from wplay.utils.helpers import create_dirs 28 | from wplay.utils.helpers import kill_child_processes 29 | # endregion 30 | 31 | 32 | def print_logo(text_logo): 33 | figlet = Figlet(font='slant') 34 | print(figlet.renderText(text_logo)) 35 | 36 | 37 | # parse positional and optional arguments 38 | def get_arg_parser(): 39 | parser = argparse.ArgumentParser(description='WhatsApp-play') 40 | parser.add_argument( 41 | "target", 42 | metavar="TARGET", 43 | type=str, 44 | default=None, 45 | nargs="?", 46 | help="""contact or group name, optional, 47 | target can be selected manually except for saving chat""") 48 | 49 | parser.add_argument('-s', '--sender', help='contact or group name') 50 | parser.add_argument('-r', '--receiver', help='contact or group name') 51 | 52 | group = parser.add_mutually_exclusive_group(required=True) 53 | group.add_argument( 54 | "-wc", 55 | "--terminal-chat", 56 | action="store_true", 57 | help="chatting from command line") 58 | 59 | group.add_argument( 60 | "-wi", 61 | "--chat-intermediator", 62 | action="store_true", 63 | help='Be an Intermediator from command line -wi -s -r ') 64 | 65 | group.add_argument( 66 | "-wpd", 67 | "--profile-download", 68 | action="store_true", 69 | help='Download the peofile picture of target') 70 | 71 | group.add_argument( 72 | "-wms", 73 | "--message-service", 74 | action="store_true", 75 | help="send messages from a JSON file") 76 | 77 | group.add_argument( 78 | "-wb", 79 | "--message-blast", 80 | action="store_true", 81 | help="message blast to a person") 82 | 83 | group.add_argument( 84 | "-wti", 85 | "--message-timer", 86 | action="store_true", 87 | help="send messages from time to time") 88 | 89 | group.add_argument( 90 | "-wt", 91 | "--online-tracker", 92 | action="store_true", 93 | help="track online status of person") 94 | 95 | group.add_argument( 96 | "-wtb", 97 | "--telegram-bot", 98 | action="store_true", 99 | help="sends tracking status to telegram bot") 100 | 101 | group.add_argument( 102 | "-wsc", 103 | "--save-chat", 104 | action="store_true", 105 | help="save all chats from Google Drive, target is necessary") 106 | 107 | group.add_argument( 108 | "-ws", 109 | "--schedule-message", 110 | action="store_true", 111 | help="send the message at scheduled time") 112 | 113 | group.add_argument( 114 | "-wa", 115 | "--about-changer", 116 | action="store_true", 117 | help="Changes the about section") 118 | 119 | group.add_argument( 120 | "-wgn", 121 | "--get-news", 122 | action="store_true", 123 | help="Get news in whatsapp group") 124 | 125 | group.add_argument( 126 | "-wgp", 127 | "--get-profile-photos", 128 | action="store_true", 129 | help="Get profile photo of all your contacts") 130 | 131 | group.add_argument( 132 | "-wbc", 133 | "--broadcast", 134 | action="store_true", 135 | help="Broadcast message") 136 | 137 | group.add_argument( 138 | "-wtf", 139 | "--target-info", 140 | action="store_true", 141 | help="finds the information about target's contact") 142 | 143 | group.add_argument( 144 | "-wd", 145 | "--download-media", 146 | action="store_true", 147 | help="Download the media of the target's contact") 148 | 149 | return parser 150 | 151 | 152 | # functions for different arguments 153 | async def get_and_match_args(parser): 154 | args = parser.parse_args() 155 | if args.online_tracker: 156 | await online_tracker.tracker(args.target) 157 | 158 | elif args.message_service: 159 | await message_service.message_service() 160 | 161 | elif args.telegram_bot: 162 | telegram_bot.telegram_status(args.target) 163 | 164 | elif args.terminal_chat: 165 | await terminal_chat.chat(args.target) 166 | 167 | elif args.chat_intermediator: 168 | await chat_intermediator.intermediary(args.sender, args.receiver) 169 | 170 | elif args.profile_download: 171 | await profile_download.get_profile_picture(args.target) 172 | 173 | elif args.broadcast: 174 | await broadcast_message.broadcast() 175 | 176 | elif args.message_blast: 177 | await message_blast.message_blast(args.target) 178 | 179 | elif args.message_timer: 180 | await message_timer.message_timer(args.target) 181 | 182 | elif args.schedule_message: 183 | await schedule_message.schedule_message(args.target) 184 | 185 | elif args.about_changer: 186 | await about_changer.about_changer() 187 | 188 | elif args.get_news: 189 | await get_news.get_news(args.target) 190 | 191 | elif args.get_profile_photos: 192 | await get_media.get_profile_photos() 193 | 194 | elif args.target_info: 195 | await target_info.target_info(args.target) 196 | 197 | elif args.download_media: 198 | await download_media.download_media(args.target) 199 | 200 | elif args.save_chat: 201 | await save_chat.save_chat(args.target) 202 | 203 | 204 | async def main(): 205 | print_logo("wplay") 206 | create_dirs() 207 | parser = get_arg_parser() 208 | try: 209 | await get_and_match_args(parser) 210 | sys.exit(0) 211 | except KeyboardInterrupt: 212 | sys.exit(0) 213 | 214 | 215 | try: 216 | asyncio.get_event_loop().run_until_complete(main()) 217 | except KeyboardInterrupt: 218 | pass 219 | except AssertionError: 220 | try: 221 | for task in asyncio.all_tasks(): 222 | task.cancel() 223 | except RuntimeError: 224 | exit() 225 | exit() 226 | finally: 227 | kill_child_processes(os.getpid()) 228 | -------------------------------------------------------------------------------- /wplay/utils/SessionManager.py: -------------------------------------------------------------------------------- 1 | # region Imports 2 | import os 3 | import stat 4 | import shutil 5 | from pathlib import Path 6 | 7 | from whaaaaat import Separator, prompt 8 | from transitions import Machine, State 9 | 10 | from wplay.utils.helpers import user_data_folder_path 11 | from wplay.utils.helpers import menu_style 12 | # endregion 13 | 14 | 15 | states = [ 16 | State(name='start'), 17 | State(name='get_user_data_filenames', on_enter='get_user_data_filenames'), 18 | State(name='prepare_questions', on_enter='prepare_questions'), 19 | State(name='get_answer_menu', on_enter='get_answer_menu'), 20 | State(name='verify_answers', on_enter='verify_answers')] 21 | 22 | transitions = [ 23 | {'trigger': 'start', 'source': '*', 'dest': 'start'}, 24 | {'trigger': 'get_user_data_filenames', 'source': 'start', 'dest': 'get_user_data_filenames'}, 25 | {'trigger': 'prepare_questions', 'source': 'get_user_data_filenames', 'dest': 'prepare_questions'}, 26 | {'trigger': 'get_answer_menu', 'source': 'prepare_questions', 'dest': 'get_answer_menu'}, 27 | {'trigger': 'verify_answers', 'source': 'get_answer_menu', 'dest': 'verify_answers'}] 28 | 29 | 30 | class SessionManager(object): 31 | def __init__(self): 32 | self.data_filenames = None # type : list 33 | self.questions_menu = None # type : list 34 | self.question_overwrite = None # type : list 35 | self.answers_menu = None # type : dict 36 | self.username = None # type : str 37 | self.save_session = False # type : bool 38 | self.user_options = { 39 | 'restore': 'Restore a session', 40 | 'save': 'Create a new session', 41 | 'continue': 'Continue without saving', 42 | 'delete': 'Delete a session', 43 | 'exit': 'Exit' 44 | } # type : dict 45 | 46 | def reset_fields(self): 47 | self.data_filenames = None # type : list 48 | self.questions_menu = None # type : list 49 | self.question_overwrite = None # type : list 50 | self.answers_menu = None # type : dict 51 | self.username = None # type : str 52 | self.save_session = False # type : bool 53 | 54 | def get_user_data_filenames(self): 55 | self.data_filenames = [file.stem for file in user_data_folder_path.glob('*')] 56 | 57 | def prepare_questions(self): 58 | self.questions_menu = [ 59 | { 60 | 'type': 'rawlist', 61 | 'name': 'user_options', 62 | 'message': '***Session Manager***:', 63 | 'choices': [ 64 | Separator(), 65 | self.user_options['restore'], 66 | self.user_options['save'], 67 | self.user_options['continue'], 68 | Separator(), 69 | self.user_options['delete'], 70 | self.user_options['exit'], 71 | Separator() 72 | ] 73 | }, 74 | { 75 | 'type': 'rawlist', 76 | 'name': 'restore', 77 | 'message': 'Select a session to try to restore:', 78 | 'choices': [*[session for session in self.data_filenames], '<---Go-back---'], 79 | 'when': lambda answers: answers['user_options'] == self.user_options['restore'] 80 | }, 81 | { 82 | 'type': 'input', 83 | 'name': 'save', 84 | 'message': 'Write your first name or username to save:', 85 | 'when': lambda answers: answers['user_options'] == self.user_options['save'] 86 | }, 87 | { 88 | 'type': 'checkbox', 89 | 'name': 'delete', 90 | 'message': 'Mark the sessions you want to delete:', 91 | 'choices': list(map(lambda e: {'name': e}, self.data_filenames)), 92 | 'when': lambda answers: answers['user_options'] == self.user_options['delete'] 93 | } 94 | ] 95 | 96 | self.question_overwrite = [ 97 | { 98 | 'type': 'confirm', 99 | 'name': 'overwrite_data', 100 | 'message': 'There is already a session with that name, overwrite it?', 101 | 'default': True 102 | } 103 | ] 104 | 105 | def get_answer_menu(self): 106 | self.answers_menu = prompt(self.questions_menu, style=menu_style) 107 | 108 | def verify_answers(self): 109 | # Handle when person choose 'Restore a session' 110 | if self.answers_menu['user_options'] == self.user_options['restore']: 111 | if self.answers_menu['restore'] == '<---Go-back---': 112 | return False 113 | else: 114 | self.username = self.answers_menu['restore'] 115 | self.save_session = True 116 | return True 117 | 118 | # Handle when person choose 'Create a new session' 119 | elif self.answers_menu['user_options'] == self.user_options['save']: 120 | self.username = self.answers_menu['save'] 121 | self.save_session = True 122 | return self.__verify_if_session_file_exists() 123 | 124 | # Handle when person choose 'Continue without saving' 125 | elif self.answers_menu['user_options'] == self.user_options['continue']: 126 | self.username, self.save_session = None, False 127 | return True 128 | 129 | # Handle when person choose 'Delete a session' 130 | elif self.answers_menu['user_options'] == self.user_options['delete']: 131 | if len(self.answers_menu['delete']) > 0: 132 | [self.__delete_session_data(user_data_folder_path / username) for username in self.answers_menu['delete']] 133 | return False 134 | 135 | # Handle when person choose 'Exit' 136 | elif self.answers_menu['user_options'] == self.user_options['exit']: 137 | exit() 138 | 139 | def __verify_if_session_file_exists(self) -> bool: 140 | if self.username in self.data_filenames: 141 | answer_overwrite = prompt(self.question_overwrite, style=menu_style) 142 | if answer_overwrite['overwrite_data']: 143 | self.__delete_session_data(user_data_folder_path / self.username) 144 | return answer_overwrite['overwrite_data'] 145 | return True 146 | 147 | def __delete_session_data(self, path): 148 | def handleError(func, path, exc_info): 149 | print('Handling Error for file ', path) 150 | if not os.access(path, os.W_OK): 151 | print('Trying to change permission!') 152 | os.chmod(path, stat.S_IWUSR) 153 | shutil.rmtree(path, ignore_errors=True) 154 | shutil.rmtree(path, onerror=handleError) 155 | 156 | @staticmethod 157 | def session_manager(): 158 | done = False 159 | obj = SessionManager() 160 | while(not done): 161 | if obj.questions_menu is not None: 162 | obj.reset_fields() 163 | machine = Machine(obj, states, transitions=transitions, initial='start') 164 | obj.get_user_data_filenames() 165 | obj.prepare_questions() 166 | obj.get_answer_menu() 167 | done = obj.verify_answers() 168 | 169 | return obj.username, obj.save_session 170 | -------------------------------------------------------------------------------- /wplay/utils/target_search.py: -------------------------------------------------------------------------------- 1 | # region TUTORIAL 2 | ''' 3 | Go to region 'FOR SCRIPTING' and use the methods in your script! 4 | 5 | EXAMPLE OF USAGE: 6 | from wplay.pyppeteerUtils import pyppeteerConfig as pypConfig 7 | from wplay.pyppeteerUtils import pyppeteerSearch as pypSearch 8 | 9 | async def my_script(target): 10 | pages, browser = wait pyp.configure_browser_and_load_whatsapp(pypConfig.websites['whatsapp']) 11 | await pypSearch.search_for_target_and_get_ready_for_conversation(pages[0], target) 12 | 13 | message = pypSearch.ask_user_for_message_breakline_mode() 14 | await pypSearch.send_message(pages[0], message) 15 | 16 | message2 = pypSearch.ask_user_for_message() 17 | await pypSearch.send_message(pages[0], message2) 18 | ''' 19 | # endregion 20 | 21 | 22 | # region IMPORTS 23 | import asyncio 24 | import time 25 | from pathlib import Path 26 | 27 | from pyppeteer.errors import ElementHandleError 28 | from pyppeteer.browser import Browser 29 | from pyppeteer.page import Page 30 | 31 | from wplay.utils.helpers import whatsapp_selectors_dict, websites 32 | from wplay.utils.browser_config import load_website 33 | from wplay.utils.Logger import Logger 34 | from wplay.utils.helpers import logs_path 35 | from wplay import target_info 36 | # endregion 37 | 38 | 39 | # region LOGGER 40 | __logger = Logger(Path(__file__).name) 41 | # endregion 42 | 43 | 44 | # region FOR SCRIPTING 45 | async def search_and_select_target_all_ways(page: Page, target: str, hide_groups: bool = False): 46 | """ 47 | Function try to search with 'search_and_select_target' function, 48 | if any error occurs we try to search with 'search_and_select_target_without_new_chat_button' function. 49 | 50 | Arguments: 51 | page {Page} -- Pyppeteer page object 52 | target {str} -- string with target name or number 53 | hide_groups {bool} -- hide or not groups from search result (default: {False}) 54 | 55 | Returns: 56 | target_focused_title {str} -- Return the target focused title 57 | """ 58 | try: 59 | if not await search_target_by_number(page, target): 60 | await search_and_select_target(page, target) 61 | except Exception as e: 62 | __logger.error(f"Error searching target: {str(e)}. Trying the second way to search.") 63 | await page.reload() 64 | await search_and_select_target_without_new_chat_button(page, target) 65 | 66 | 67 | async def search_target_by_number(page: Page, target: str): 68 | if await __try_load_contact_by_number(page, target): 69 | # target_focused_title = await __get_focused_target_title(page, target) 70 | # await __display_complete_target_info(page,choosed_target,contact_tuple) 71 | await __wait_for_message_area(page) 72 | return True 73 | else: 74 | return False 75 | 76 | 77 | async def search_and_select_target(page: Page, target: str, hide_groups: bool = False): 78 | """ 79 | Function search for targets using the whatsapp new chat button. 80 | When this function print the list of target found it doesn't print the phone number, 81 | the phone is printed only after you choose the target. 82 | 83 | Arguments: 84 | page {Page} -- Pyppeteer page object 85 | target {str} -- string with target name or number 86 | hide_groups {bool} -- hide or not groups from search result (default: {False}) 87 | 88 | Returns: 89 | target_focused_title {str} -- Return the target focused title 90 | """ 91 | 92 | await __open_new_chat(page) 93 | await __type_in_new_chat_search_bar(page, target) 94 | contact_list_elements_unchecked = await __get_contacts_elements_filtered(page, target) 95 | group_list_elements_unchecked = await __get_groups_elements_filtered(page, target, hide_groups) 96 | contact_titles_unchecked = await __get_contacts_titles_from_elements_unchecked(page, contact_list_elements_unchecked) 97 | group_titles_unchecked = await __get_groups_titles_from_elements_unchecked(page, group_list_elements_unchecked) 98 | contact_list_unchecked = __zip_contact_titles_and_elements_unchecked(contact_titles_unchecked, contact_list_elements_unchecked) 99 | group_list_unchecked = __zip_group_titles_and_elements_unchecked(group_titles_unchecked, group_list_elements_unchecked) 100 | contact_tuple = __check_contact_list(target, contact_list_unchecked) 101 | group_tuple = __check_group_list(target, group_list_unchecked) 102 | target_tuple = __get_target_tuple(contact_tuple, group_tuple) 103 | __print_target_tuple(target_tuple) 104 | target_index_choosed = __ask_user_to_choose_the_filtered_target(target_tuple) 105 | choosed_target = __get_choosed_target(target_tuple, target_index_choosed) 106 | await __navigate_to_target(page, choosed_target) 107 | target_focused_title = await __get_focused_target_title(page, target) 108 | await __display_complete_target_info(page, choosed_target, contact_tuple) 109 | __check_target_focused_title(target, target_focused_title) 110 | await __wait_for_message_area(page) 111 | return target_focused_title 112 | 113 | 114 | async def search_and_select_target_without_new_chat_button(page: Page, target: str, hide_groups: bool = False): 115 | """ 116 | Function search for targets search bar in the whatsapp, without using the new chat button. 117 | Here we don't look for the contact by the number, we look for the number as if it were a string, 118 | just to be an alternative to the method used in the other function. 119 | When this function print the list of target found it print the phone number of every target, 120 | the phone is also printed after you choose the target. 121 | 122 | Arguments: 123 | page {Page} -- Pyppeteer page object 124 | target {str} -- string with target name or number 125 | hide_groups {bool} -- hide or not groups from search result (default: {False}) 126 | 127 | Returns: 128 | target_focused_title {str} -- Return the target focused title 129 | """ 130 | await __type_in_chat_or_message_search(page, target) 131 | chats_messages_groups_elements_list = await __get_chats_messages_groups_elements(page) 132 | contact_name_index_tuple_list = await __get_contacts_matched_with_query(chats_messages_groups_elements_list) 133 | group_name_index_tuple_list = await __get_groups_matched_with_query(chats_messages_groups_elements_list, hide_groups) 134 | await __get_number_of_filtered_contacts(page, contact_name_index_tuple_list, chats_messages_groups_elements_list) 135 | target_tuple = __get_target_tuple(contact_name_index_tuple_list, group_name_index_tuple_list) 136 | __print_target_tuple(target_tuple) 137 | target_index_chosen = __ask_user_to_choose_the_filtered_target(target_tuple) 138 | 139 | # chosen_target will be a tuple (a,b) such that a is the name of the target and b is the 140 | # index of that element in chats_messages_groups_elements_list 141 | 142 | chosen_target = __get_choosed_target(target_tuple, target_index_chosen) 143 | await __open_selected_chat(chosen_target[1], chats_messages_groups_elements_list) 144 | target_name = chosen_target[0] 145 | await __display_complete_target_info(page, chosen_target, contact_name_index_tuple_list) 146 | await __wait_for_message_area(page) 147 | return target_name 148 | # endregion 149 | 150 | 151 | # region SEARCH AND SELECT TARGET 152 | async def __accept_dialog(dialog): 153 | try: 154 | await dialog.accept() 155 | except: 156 | pass 157 | 158 | 159 | async def __try_load_contact_by_number(page: Page, target: str) -> bool: 160 | try: 161 | if int(target): 162 | __logger.debug("Loading contact by number.") 163 | 164 | page.on( 165 | 'dialog', 166 | lambda dialog: asyncio.ensure_future(__accept_dialog(dialog)) 167 | ) 168 | await load_website(page, f"{websites['wpp_unknown']}{target}") 169 | time.sleep(2) 170 | if (await page.evaluate(f'document.querySelector("{whatsapp_selectors_dict["invalid_number_ok_button"]}") != null')): 171 | await page.click(whatsapp_selectors_dict["invalid_number_ok_button"]) 172 | __logger.debug(f"Invalid number: {target}") 173 | print(f"Invalid Number: {target}") 174 | return False 175 | return True 176 | except Exception as e: 177 | __logger.error(f"Error loading contact by number: {str(e)}") 178 | return False 179 | return False 180 | 181 | 182 | async def __get_number_of_filtered_contacts(page: Page, contact_name_index_tuple_list: list, chats_messages_groups_elements_list: list): 183 | try: 184 | for index, contact_name_index_tuple in enumerate(contact_name_index_tuple_list): 185 | await chats_messages_groups_elements_list[contact_name_index_tuple[1]].click() 186 | await __open_target_chat_info_page(page) 187 | contact_page_elements = await __get_contact_page_elements(page) 188 | complete_target_info = {} 189 | await __get_contact_about_and_phone(contact_page_elements[3], complete_target_info) 190 | contact_name_index_tuple_list[index] = (contact_name_index_tuple[0] + " : " + complete_target_info['Mobile'], contact_name_index_tuple[1]) 191 | await __close_contact_info_page(page) 192 | except Exception as e: 193 | print(e) 194 | 195 | 196 | async def __display_complete_target_info(page, target_tuple, contact_tuple): 197 | complete_target_info = {} 198 | try: 199 | if any(target_tuple[0] in i for i in contact_tuple): 200 | complete_target_info = await __get_complete_info_on_target(page) 201 | __print_complete_target_info(complete_target_info) 202 | await __close_contact_info_page(page) 203 | else: 204 | complete_target_info = await __get_complete_info_on_group(page) 205 | __print_complete_target_info(complete_target_info) 206 | await __close_contact_info_page(page) 207 | except Exception as e: 208 | print(e) 209 | 210 | 211 | async def __type_in_chat_or_message_search(page, target): 212 | try: 213 | print(f'Looking for: {target}') 214 | await page.waitForSelector( 215 | whatsapp_selectors_dict['chat_or_message_search'], 216 | visible=True, 217 | timeout=0 218 | ) 219 | await page.waitFor(500) 220 | await page.type(whatsapp_selectors_dict['chat_or_message_search'], target) 221 | await page.waitFor(3000) 222 | except Exception as e: 223 | print(e) 224 | 225 | 226 | async def __get_chats_messages_groups_elements(page: Page): 227 | chats_messages_groups_elements_list = [] # type : list[int] 228 | try: 229 | chats_messages_groups_elements_list = await page.querySelectorAll(whatsapp_selectors_dict['chats_groups_messages_elements']) 230 | return chats_messages_groups_elements_list 231 | except Exception as e: 232 | print(e) 233 | exit() 234 | 235 | 236 | async def __get_contacts_matched_with_query(chats_groups_messages_elements_list: list): 237 | contacts_to_choose_from = [] # type : list[str , int] 238 | get_contact_node_title_function = 'node => node.parentNode.getAttribute("title")' 239 | for idx, element in enumerate(chats_groups_messages_elements_list): 240 | try: 241 | contact_name = await element.querySelectorEval(whatsapp_selectors_dict['contact_element'], get_contact_node_title_function) 242 | if contact_name is not None: 243 | contacts_to_choose_from.append((contact_name, idx)) 244 | except ElementHandleError: 245 | # if it is not a contact element, move to the next one 246 | continue 247 | except Exception as e: 248 | print(e) 249 | 250 | return contacts_to_choose_from 251 | 252 | 253 | async def __get_groups_matched_with_query(chats_groups_messages_elements_list: list, hide_groups: bool): 254 | groups_to_choose_from = [] 255 | 256 | if hide_groups: 257 | return groups_to_choose_from 258 | 259 | get_group_node_title_function = 'node => node.parentNode.getAttribute("title")' 260 | for idx, element in enumerate(chats_groups_messages_elements_list): 261 | try: 262 | group_name = await element.querySelectorEval(whatsapp_selectors_dict['group_element'], 263 | get_group_node_title_function) 264 | groups_to_choose_from.append((group_name,idx)) 265 | except ElementHandleError: 266 | # if it is not a contact element, move to the next one 267 | continue 268 | except Exception as e: 269 | print(e) 270 | 271 | return groups_to_choose_from 272 | 273 | 274 | async def __open_selected_chat(target_index: int, chats_messages_groups_elements_list: list): 275 | try: 276 | await chats_messages_groups_elements_list[target_index].click() 277 | except Exception as e: 278 | print(f"This target doesn't exist! Error: {str(e)}") 279 | exit() 280 | 281 | 282 | async def __get_complete_info_on_target(page: Page): 283 | contact_page_elements = [] 284 | try: 285 | await __open_target_chat_info_page(page) 286 | contact_page_elements = await __get_contact_page_elements(page) 287 | complete_target_info = {} 288 | await __get_contact_name_info(contact_page_elements[0], complete_target_info) 289 | await __get_contact_about_and_phone(contact_page_elements[3], complete_target_info) 290 | await __get_contact_groups_common_with_target(complete_target_info, page) 291 | return complete_target_info 292 | except Exception as e: 293 | print(e) 294 | 295 | 296 | async def __get_complete_info_on_group(page): 297 | try: 298 | await __open_target_chat_info_page(page) 299 | contact_page_elements = await __get_contact_page_elements(page) 300 | complete_target_group_info = {} 301 | await __get_target_group_name(contact_page_elements[0],complete_target_group_info) 302 | await __get_target_group_creation_info(contact_page_elements[0],complete_target_group_info) 303 | await __get_target_group_description(contact_page_elements[1],complete_target_group_info) 304 | await __get_target_group_members(contact_page_elements[4],complete_target_group_info) 305 | return complete_target_group_info 306 | except Exception as e: 307 | print(e) 308 | 309 | 310 | async def __get_target_group_name(element, complete_target_info): 311 | try: 312 | get_inner_text_function = 'e => e.innerText' 313 | complete_target_info['Name'] = await element.querySelectorEval(whatsapp_selectors_dict['contact_info_page_target_group_name_element'],get_inner_text_function) 314 | except Exception as e: 315 | print(e) 316 | 317 | 318 | async def __get_target_group_creation_info(element, complete_target_info): 319 | try: 320 | get_inner_text_function = 'e => e.innerText' 321 | complete_target_info['Creation Info'] = await element.querySelectorEval\ 322 | (whatsapp_selectors_dict['contact_info_page_target_group_creation_info_element'],get_inner_text_function) 323 | except Exception as e: 324 | print(e) 325 | 326 | 327 | async def __get_target_group_description(element,complete_target_info): 328 | try: 329 | get_inner_text_function = 'e => e.innerText' 330 | complete_target_info['Description'] = await element.querySelectorEval \ 331 | (whatsapp_selectors_dict['contact_info_page_target_group_description_element'], get_inner_text_function) 332 | except Exception as e: 333 | print(e) 334 | 335 | 336 | async def __get_target_group_members(element, complete_target_info): 337 | try: 338 | children_elements = await element.querySelectorAll(':scope > div') 339 | if len(children_elements) <= 2: 340 | target_group_members_selector = ':scope > div:last-child > div > div' 341 | elif len(children_elements) == 3: 342 | target_group_members_selector = ':scope > div:nth-child(2) > div > div' 343 | else: 344 | target_group_members_selector = whatsapp_selectors_dict['contact_info_page_target_group_member_elements'] 345 | 346 | target_group_member_elements = await element.querySelectorAll(target_group_members_selector) 347 | 348 | group_member_name_selector = ':scope span[title]' 349 | get_element_title_function = 'e => e.getAttribute("title")' 350 | complete_target_info['Members'] = [await ele.querySelectorEval(group_member_name_selector,get_element_title_function) 351 | for ele in target_group_member_elements] 352 | except Exception as e: 353 | print(e) 354 | 355 | 356 | async def __open_target_chat_info_page(page): 357 | try: 358 | await page.waitForSelector( 359 | whatsapp_selectors_dict['target_chat_header'], 360 | visible=True, 361 | timeout=3000 362 | ) 363 | await page.click(whatsapp_selectors_dict['target_chat_header']) 364 | except Exception as e: 365 | print(e) 366 | 367 | 368 | async def __get_contact_page_elements(page: Page): 369 | contact_page_elements = [] 370 | try: 371 | await page.waitForSelector( 372 | whatsapp_selectors_dict['contact_info_page_elements'], 373 | visible=True, 374 | timeout=8000 375 | ) 376 | contact_page_elements = await page.querySelectorAll(whatsapp_selectors_dict['contact_info_page_elements']) 377 | return contact_page_elements 378 | except Exception as e: 379 | print(e) 380 | 381 | 382 | async def __get_contact_name_info(contact_name_element,complete_target_info): 383 | try: 384 | complete_target_info['Name'] = await contact_name_element.querySelectorEval('span > span', 'element => element.innerText') 385 | complete_target_info['Last_seen'] = await contact_name_element.querySelectorEval('div > span:last-of-type > div > span', 'element => element.getAttribute("title")') 386 | except: 387 | print(f'last seen not available') 388 | 389 | 390 | async def __get_contact_about_and_phone(contact_name_element, complete_target_info): 391 | try: 392 | complete_target_info['About'] = await contact_name_element.querySelectorEval('div:nth-child(2) > div > div > span > span', 'element => element.getAttribute("title")') 393 | complete_target_info['Mobile'] = await contact_name_element.querySelectorEval('div:last-of-type > div > div > span > span', 'element => element.innerText') 394 | target_info.target_contact_number(complete_target_info['Mobile']) 395 | except Exception as e: 396 | print(e) 397 | 398 | 399 | async def __get_contact_groups_common_with_target(complete_target_info,page): 400 | try: 401 | await page.waitForSelector( 402 | whatsapp_selectors_dict['contact_info_page_group_element_heading'], 403 | visible=True, 404 | timeout=3000 405 | ) 406 | 407 | if (await page.evaluate(f'document.querySelector("{whatsapp_selectors_dict["contact_info_page_group_element_heading"]}").innerText'))\ 408 | == "Groups in common": 409 | group_elements = await page.querySelectorAll(whatsapp_selectors_dict['contact_info_page_group_elements']) 410 | complete_target_info['Groups'] = [await ele.querySelectorEval('div>div>div:nth-child(2)>div:first-child>div>div>span', 'e => e.getAttribute("title")') for ele in group_elements] 411 | else: 412 | complete_target_info['Groups'] = [] 413 | except: 414 | complete_target_info['Groups'] = [] 415 | print(f'No groups in common') 416 | 417 | 418 | async def __close_contact_info_page(page: Page): 419 | try: 420 | await page.waitForSelector( 421 | whatsapp_selectors_dict['contact_info_page_close_button'], 422 | visible=True, 423 | timeout=5000 424 | ) 425 | await page.click(whatsapp_selectors_dict['contact_info_page_close_button']) 426 | except Exception as e: 427 | print(e) 428 | 429 | 430 | def __print_complete_target_info(complete_target_info): 431 | for key in complete_target_info.keys(): 432 | if key == "Groups" or key == "Members": 433 | print(key + ":") 434 | print(*complete_target_info[key], sep=",") 435 | else: 436 | print(f'{key}: {complete_target_info[key]} ') 437 | 438 | 439 | async def __open_new_chat(page: Page): 440 | await page.waitForSelector( 441 | whatsapp_selectors_dict['new_chat_button'], 442 | visible=True, 443 | timeout=0 444 | ) 445 | await page.waitFor(500) 446 | await page.click(whatsapp_selectors_dict['new_chat_button']) 447 | 448 | 449 | async def __type_in_new_chat_search_bar(page: Page, target: str): 450 | print(f'Looking for: {target}') 451 | __logger.info('Searching Target') 452 | await page.waitForSelector( 453 | whatsapp_selectors_dict['search_contact_input_new_chat'], 454 | visible=True 455 | ) 456 | await page.type(whatsapp_selectors_dict['search_contact_input_new_chat'], target) 457 | await page.waitFor(3000) 458 | 459 | 460 | async def __get_contacts_elements_filtered(page: Page, target: str): 461 | contact_list_elements_unchecked = list() 462 | try: 463 | await page.waitForSelector( 464 | whatsapp_selectors_dict['contact_list_elements_filtered_new_chat'], 465 | visible=True, 466 | timeout=3000 467 | ) 468 | 469 | contact_list_elements_unchecked = await page.querySelectorAll(whatsapp_selectors_dict['contact_list_elements_filtered_new_chat']) 470 | except: 471 | print(f'No contact named by "{target}"!') 472 | __logger.info('Target not found') 473 | return contact_list_elements_unchecked 474 | 475 | 476 | async def __get_groups_elements_filtered(page: Page, target: str, hide_groups: bool = False): 477 | group_list_elements_unchecked = list() 478 | 479 | if hide_groups: 480 | return group_list_elements_unchecked 481 | 482 | try: 483 | await page.waitForSelector( 484 | whatsapp_selectors_dict['group_list_elements_filtered_new_chat'], 485 | visible=True, 486 | timeout=3000 487 | ) 488 | 489 | group_list_elements_unchecked = await page.querySelectorAll(whatsapp_selectors_dict['group_list_elements_filtered_new_chat']) 490 | except: 491 | print(f'No group named by "{target}"!') 492 | __logger.info('Target not found in groups') 493 | return group_list_elements_unchecked 494 | 495 | 496 | async def __get_contacts_titles_from_elements_unchecked(page: Page, contact_list_elements_unchecked: list): 497 | contact_titles_unchecked = [] 498 | for i in range(len(contact_list_elements_unchecked)): 499 | contact_titles_unchecked.append(await page.evaluate(f'document.querySelectorAll("{whatsapp_selectors_dict["contact_list_elements_filtered_new_chat"]}")[{i}].getAttribute("title")')) 500 | return contact_titles_unchecked 501 | 502 | 503 | async def __get_groups_titles_from_elements_unchecked(page: Page, group_list_elements_unchecked: list): 504 | group_titles_unchecked = [] 505 | for i in range(len(group_list_elements_unchecked)): 506 | group_titles_unchecked.append(await page.evaluate(f'document.querySelectorAll("{whatsapp_selectors_dict["group_list_elements_filtered_new_chat"]}")[{i}].getAttribute("title")')) 507 | return group_titles_unchecked 508 | 509 | 510 | # contact_list_unchecked is a zip (list of tuples) of contact_titles and 511 | # contact elements, unchecked. 512 | def __zip_contact_titles_and_elements_unchecked(contact_titles_unchecked, contact_list_elements_unchecked): 513 | contact_list_unchecked = list(zip(contact_titles_unchecked, contact_list_elements_unchecked)) 514 | return contact_list_unchecked 515 | 516 | 517 | def __zip_group_titles_and_elements_unchecked(group_titles_unchecked, group_list_elements_unchecked): 518 | group_list_unchecked = list(zip(group_titles_unchecked, group_list_elements_unchecked)) 519 | return group_list_unchecked 520 | 521 | 522 | # __checking_contact_list verify if target is in title, if not we pop from list 523 | def __check_contact_list(target: str, contact_list_unchecked): 524 | i = 0 525 | while i < len(contact_list_unchecked): 526 | if len(contact_list_unchecked) <= 0: 527 | break 528 | 529 | # we can add more verifications if we are getting false-positive contacts 530 | if contact_list_unchecked[i][0].lower().find(target.lower()) == -1: 531 | try: 532 | contact_list_unchecked.pop(i) 533 | except Exception as e: 534 | print(f'Error: {str(e)}') 535 | i -= 1 536 | i += 1 537 | 538 | contact_tuple = tuple(contact_list_unchecked) 539 | return contact_tuple 540 | 541 | 542 | def __check_group_list(target: str, group_list_unchecked): 543 | i = 0 544 | while i < len(group_list_unchecked): 545 | if len(group_list_unchecked) <= 0: 546 | break 547 | 548 | # we can add more verifications if we are getting false-positive groups 549 | if group_list_unchecked[i][0].lower().find(target.lower()) == -1: 550 | try: 551 | group_list_unchecked.pop(i) 552 | except Exception as e: 553 | print(f'Error: {str(e)}') 554 | i -= 1 555 | i += 1 556 | 557 | group_tuple = tuple(group_list_unchecked) 558 | return group_tuple 559 | 560 | 561 | # target_list is like that: (((0, 'a'), (1, 'b')), ((3, 'c'), (4, 'd'))), 562 | # but instead numbers and letters we have titles and elements 563 | # the first index is the contacts and the second is the groups 564 | def __get_target_tuple(contact_tuple, group_tuple): 565 | target_tuple = (contact_tuple, group_tuple) 566 | # check to see if the target exits in the user's address book 567 | if len(target_tuple[0]) == 0 and len(target_tuple[1]) == 0: 568 | print('The target does not exist, please enter a valid target name') 569 | __logger.error('Invalid target name entered') 570 | exit() 571 | return target_tuple 572 | 573 | 574 | def __print_target_tuple(target_tuple): 575 | lenght_of_contacts_tuple = len(target_tuple[0]) 576 | lenght_of_groups_tuple = len(target_tuple[1]) 577 | 578 | for i in range(lenght_of_contacts_tuple): 579 | if lenght_of_contacts_tuple <= 0: 580 | break 581 | if i == 0: 582 | print("Contacts found:") 583 | __logger.info('List of Targets') 584 | print(f'{i}: {target_tuple[0][i][0]}') 585 | 586 | for i in range(lenght_of_contacts_tuple, lenght_of_groups_tuple + lenght_of_contacts_tuple): 587 | if lenght_of_groups_tuple <= 0: 588 | break 589 | if i == lenght_of_contacts_tuple: 590 | print("Groups found:") 591 | __logger.info('List of Target in groups') 592 | print(f'{i}: {target_tuple[1][i-lenght_of_contacts_tuple][0]}') 593 | 594 | 595 | def __ask_user_to_choose_the_filtered_target(target_tuple): 596 | if len(target_tuple[0] + target_tuple[1]) > 0: 597 | __logger.info('Input Target Number') 598 | target_index_choosed = int( 599 | input('Enter the number of the target you wish to choose: ')) 600 | return target_index_choosed 601 | 602 | 603 | def __get_choosed_target(target_tuple, target_index_choosed): 604 | lenght_of_contacts_tuple = len(target_tuple[0]) 605 | if target_index_choosed is None: 606 | exit() 607 | 608 | try: 609 | if target_index_choosed < lenght_of_contacts_tuple: 610 | choosed_target = target_tuple[0][target_index_choosed] 611 | elif target_index_choosed >= lenght_of_contacts_tuple: 612 | choosed_target = target_tuple[1][target_index_choosed - lenght_of_contacts_tuple] 613 | else: 614 | print("This target doesn't exist!") 615 | __logger.error('Invalid Target') 616 | exit() 617 | except Exception as e: 618 | print(f"This target doesn't exist! Error: {str(e)}") 619 | __logger.error('Invalid Target') 620 | exit() 621 | return choosed_target 622 | 623 | 624 | async def __navigate_to_target(page: Page, choosed_target): 625 | try: 626 | await choosed_target[1].click() 627 | except Exception as e: 628 | print(f"This target doesn't exist! Error: {str(e)}") 629 | __logger.error('Invalid Target') 630 | exit() 631 | 632 | 633 | async def __get_focused_target_title(page: Page, target): 634 | try: 635 | await page.waitForSelector(whatsapp_selectors_dict['target_focused_title']) 636 | target_focused_title = await page.evaluate(f'document.querySelector("{whatsapp_selectors_dict["target_focused_title"]}").getAttribute("title")') 637 | except Exception as e: 638 | print(f'No target selected! Error: {str(e)}') 639 | __logger.error('Target not selected from list') 640 | exit() 641 | return target_focused_title 642 | 643 | 644 | def __print_selected_target_title(target_focused_title): 645 | print(f"You've selected the target named by: {target_focused_title}") 646 | __logger.info('Selected Target') 647 | 648 | 649 | def __check_target_focused_title(target, target_focused_title): 650 | """if int(target): 651 | def only_numerics(seq): 652 | seq_type= type(seq) 653 | return seq_type().join(filter(seq_type.isdigit, seq)) 654 | 655 | target = only_numerics(target) 656 | target_focused_title = only_numerics(target_focused_title) 657 | 658 | if target_focused_title.strip().find(target) == -1: 659 | print(f"Maybe you're focused in the wrong target, {target_focused_title}") 660 | must_continue = str(input("Do you want to continue (yes/no)? ")) 661 | accepted_yes = {'yes', 'y'} 662 | if not must_continue.lower() in accepted_yes: 663 | exit() 664 | """ 665 | if target_focused_title.lower().find(target.lower()) == -1: 666 | print(f"You're focused in the wrong target, {target_focused_title}") 667 | must_continue = str(input("Do you want to continue (yes/no)? ")) 668 | accepted_yes = {'yes', 'y'} 669 | if not must_continue.lower() in accepted_yes: 670 | exit() 671 | 672 | 673 | async def __wait_for_message_area(page: Page): 674 | try: 675 | await page.waitForSelector(whatsapp_selectors_dict['message_area']) 676 | except Exception as e: 677 | print(f"You don't belong this group anymore! Error: {str(e)}") 678 | # endregion 679 | --------------------------------------------------------------------------------