diff --git a/conf.json b/conf.json new file mode 100644 index 0000000..7053553 --- /dev/null +++ b/conf.json @@ -0,0 +1,9 @@ +{ + "default_xpath_text_area": "//footer/div/div[2]/div/div[2]", + "default_xpath_authenticated": "//*[text() = 'Keep your phone connected']", + "default_xpath_searchbar":"//div[@id='side']/div/div/label/div/div[2]", + "default_xpath_send_button":"/html/body/div[1]/div/div/div[4]/div/footer/div[1]/div[3]/button", + "default_xpath_target_user":"//span[contains(.,'%s')]", + "control_user": {"user":"Enrique","currency_format":"$","bank_account":"4531-2321-3421-3421"}, + "kill_on_auth":"0" +} diff --git a/guide.md b/guide.md new file mode 100644 index 0000000..0dd7e08 --- /dev/null +++ b/guide.md @@ -0,0 +1,90 @@ +## Glossary + +whatsapp_debt_framework : +Arguments +- production - Optional | Default : True +- message - Required - Raises ValueError if None example : Invalid Message Can Not Be None, Empty, or Null! +- debug - Optional - Default True - Turns Verbose on +- debug_data - Optional - Default False - Switches from database mode to importing json data from debtor_data.json + +Public methods: +- self.production - +- self.url_bin - +- self.notice - +- self.msg_count - + +Private methods: +- self.queue - +- self._kill_on_auth - +- self._default_xpath_text_area - +- self._default_xpath_authenticated - +- self._callbacks - +- self._control_user - + +Public Functions: +- self.collect - + Arguments + - + Returns + - + +- self.authenticate + Arguments + - None + + Returns + - + +- self.connect - # Depreciated + Arguments + -None + + Returns + - + +- self.get_message - + Arguments + -target_data - required list(target,amount) Strict + -message - Optional -Type: Str - Has preset text if left default - Default : 'default.upper()' + + Returns + - + +Private Functions: +- self._logit - + Arguments + - log_text - Required - Type: Str + - verbose - Optional - Default False - Print or just Return logs (T/F) Overridden by self.debug + Returns + - +- self._set_chrome_options - Sets browser to headless and screen size to 1920x1080 Mac OSX headers + Arguments + - None + Returns + - +## About +Payment Gateways: +- N/A + + +
+ +## Usage + +Using the whats app debt framework is very straight forward. + +example : + +TARGETS = {'name': amount} # yep it is that easy, format the debtor and the value here and run! +whatsapp_debt_framework().start(targets=TARGETS) + +name = str +amount = int + +calling start() will automatically open an instance of chrome and request you scan the qr code for your whatsapp on your phone. +Once authenticated the framework will continue through your dictionary of 'TARGETS' and send a message to each debtor about the amount using a default message, if you want to personalize what message goes into the chat windows, refer to the following example: + + +TARGETS = {'name': amount} # yep it is that easy, format the debtor and the value here and run! +message = '{name} Don't forget you have an outstanding balance with Apple for {amount}' +whatsapp_debt_framework().start(targets=TARGETS,message=message) diff --git a/readme.md b/readme.md index 1f26d05..7b9b915 100644 --- a/readme.md +++ b/readme.md @@ -1,10 +1,22 @@ # WhatsApp Debt (in progress) +Stable Releases: + +OS: + +Ubuntu 18.04 LTS [TESTED] : Working no Issues + +Else: + +Not Tested + + [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) > Automating the tedious task of collecting debts 💰 ### Todos + BASE - [ ] Add CLI args to turn on/off production mode - [ ] Add cronfile to specify the time the bot will run @@ -12,7 +24,20 @@ - [ ] Add deployment instructions - [ ] Update readme - [ ] Add GIF demo +- [ ] Add GIF Demos for conf.json with Screenshots and guide on how to use selenium ide to fetch xpath fields. - [x] Add MIT license +- [ ] Depreciate Connect until Persistence issues are fixed +- [ ] Scope, Rename, Finish Alpha Collect() +- [x] Complete Alpha Stages of Authenticate() +- [x] Temp Depreciated connect +- [ ] Create private function (in init) that loads configs +- [x] Create conf.json file +- [ ] Restructure application to a proper file hierarchy + + EXTENDED + + [ ] Resolve issues with solving the QR via image (driver.element.screenshot(filename='name.png')) when displaying on local machine or web, can not verify + [ ] Find Solution for persistent Headless Login ### Roadmap @@ -21,3 +46,74 @@ - [ ] Create app & database to dynamically add or remove people from debt **Feel free to collaborate!** +For information about Issues see issue_guide.md + + + +### For Usage Guides Check guide.md! +### For Issue Guide check out issue_guide.md + +This platform was built with the idea of making collection of automated payments through whats app an easy process. A Final version of this platform should be applicable to facilitate P2P Loans with Ease. This system should also be able to be adapted for instance for monthly payments, if a store were to collect 29.99 every 31 days, this can be a general use case for this type of platform. + + + + +### Installation & Deployment + + +Step 1: Download the source code to your local working directory + +Step 2: cd into the directory and create a virtual environemnt called wdf + +Step 3: activate your virtual env + +Step 4: install the requirements with pip install -r requirements.txt + +Step 5: Navigate to conf.json Replace control_user information with relevant information + +Step 6: Navigate to unit_test.py and Complete the TARGET information + +Step 7: Navigate to your console and run python3 unit_test.py + +Your done! + + + + +### Notes + +An example of using the platform in it's default form, is in unit_test.py, the way this framework was built is to do all of the heavy lifting for you, feed the target, amount, and authenticate it will handle the rest for you. + + +An example of using a Scheduler with this type of application to run every x time frame and so on, is in scheduler.py, A more advanced tutorial will be made available in latter versions. + + +###Useful Information + + +conf.json is per say the configuration file for this framework, you will notice these elements at the time of writing : + + + +{ + "default_xpath_text_area": "//footer/div/div[2]/div/div[2]", + "default_xpath_authenticated": "//*[text() = 'Keep your phone connected']", + "default_xpath_searchbar":"//div[@id='side']/div/div/label/div/div[2]", + "default_xpath_send_button":"//div[3]/button/span", + "default_xpath_target_user":"//span[contains(.,'%s')]", + "control_user": {"user":"Enrique","currency_format":"$","bank_account":"4531-2321-3421-3421"}, + "kill_on_auth":"0" +} + + +The First element default_xpath_text_area Defines : The chat space to use the send_keys() functions to send a message in whatsapp web (authenticated) + +The Second Element default_xpath_authenticated Defines: A point of the whatsapp website that undeniably proves you are authenticated e.g the search bar. + +The Third Element default_xpath_searchbar Defines: The Search bar behind whatsapp web (authenticated) + +The Fourth Element default_xpath_send_button Defines: The send a message button in a whatsapp chat + +The Fifth Element default_xpath_target_user Defines : The xpath used to identify a web element with the text of the target user for per say the chat window we need to click + +The Sixth Element control_user Defines: Default control user settings that defines who the debt collector is (user), The currency format ($), and the bank account associated (will be changed later for other payment methods) diff --git a/scheduler.py b/scheduler.py new file mode 100644 index 0000000..0698917 --- /dev/null +++ b/scheduler.py @@ -0,0 +1,18 @@ +from whatsapp_framework import whatsapp_debt_framework +import schedule +import time + +# pip install scheduler + +TARGETS = {'GAGAN-DEV': 10} # yep it is that easy, format the debtor and the value here and run! + + +def job(): + return whatsapp_debt_framework()._logit(f'Running Job...'),whatsapp_debt_framework().start(targets=TARGETS) + +schedule.every().day.at('15:16').do(job) #24hr format + +whatsapp_debt_framework()._logit(f'Waiting for Scheduled Jobs to Trigger...') +while True: + schedule.run_pending() + time.sleep(1) # reduce CPU load with a wait diff --git a/script.py b/script.py index b8449ee..beff2e7 100644 --- a/script.py +++ b/script.py @@ -1,3 +1,4 @@ +# DEPRECIATED! from urllib.parse import quote from time import sleep from pyperclip import copy @@ -24,13 +25,14 @@ def get_message(target): chrome_options.add_argument( 'user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36' ) -browser = webdriver.Chrome(chrome_options=chrome_options) +browser = webdriver.Chrome(chrome_options=chrome_options,executable_path='/usr/local/bin/chromedriver') browser.get('https://web.whatsapp.com') while SCANNED is False: image = browser.find_element_by_tag_name('img') image_src = image.get_attribute('src') encoded = quote(image_src, safe='') url = HOST + '/' + encoded + print(url) copy(url) print('\nLink copied to your clipboard, you got 20 seconds to visit it and scan your QR code.') print('Waiting QR Scanning...') diff --git a/unit_test.py b/unit_test.py new file mode 100644 index 0000000..270d234 --- /dev/null +++ b/unit_test.py @@ -0,0 +1,7 @@ +from whatsapp_framework import whatsapp_debt_framework + + +# pip install scheduler + +TARGETS = {'GAGAN-DEV': 10} # yep it is that easy, format the debtor and the value here and run! +whatsapp_debt_framework().start(targets=TARGETS) diff --git a/whatsapp_framework.py b/whatsapp_framework.py new file mode 100644 index 0000000..9ce7c32 --- /dev/null +++ b/whatsapp_framework.py @@ -0,0 +1,181 @@ +from urllib.parse import quote +import traceback +from time import sleep,time +from datetime import datetime +from pyperclip import copy +import random as r +import json +from selenium import webdriver +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.keys import Keys +from selenium.webdriver.chrome.options import Options +from selenium.common.exceptions import NoSuchElementException +from selenium.webdriver.support import expected_conditions as EC + +class whatsapp_debt_framework: + + def __init__(self,production=True,message=None,debug=True,debug_data=False): + if production is False: + if debug_data is True: + with open('debtor_data.json') as debtor_data: + self.debtor_data = json.load(debtor_data) + self._consume_json_data = True + with open('conf.json') as conf: + paramaters = json.load(conf) + self.debug = debug # _logit -> verbose + if debug: + message = 'None' + if message is None: + raise ValueError('Invalid Message Can Not Be None, Empty, or Null!') + else: + self.message = message + + #public + self.production = production # False + self.url_bin = {'whats_app':'https://web.whatsapp.com/'} + self.notice = {'html_key':'iHhHL','text':'Keep your phone connected'} + self.msg_count = 0 + self.data_wait = r.choice([0.22,0.3,0.25,0.23,0.34,0.35]) + + #private + # self._queue = {} # Future Release for Micro services + self._kill_on_auth = int(paramaters['kill_on_auth']) + self._clock = [] + self._default_xpath_text_area = paramaters['default_xpath_text_area'] + self._default_xpath_target_user = paramaters['default_xpath_target_user'] + self._default_xpath_authenticated = paramaters['default_xpath_authenticated'] + self._default_xpath_searchbar = paramaters['default_xpath_searchbar'] + self._default_xpath_send_button = paramaters['default_xpath_send_button'] + self._callbacks = {} # used for internal key based data & frame management. + self._control_user = paramaters['control_user'] + + def _logit(self,log_text: str,verbose=False): + if self.debug: + verbose = True + """ + A custom logging function + :param (str) log_text: The text that you want to log + :return: prints the current timestamp with the log_text after it + """ + if verbose is True: + return print(str(datetime.fromtimestamp(time())) + '\t' + log_text) + else: + return str(datetime.fromtimestamp(time())) + '\t' + log_text + + def _safe_exit_on_error(self,error=None,session=None,verbose=True): + if self.debug: # true = print stack + if error is not None: + self._logit(f'Noticed Exception : [{error.__traceback__}]') + if session is not None: + return [session.quit(),exit()] + else: + return exit() + + def _set_chrome_options(self): + chrome_options = Options() + chrome_options.add_argument('--headless') + chrome_options.add_argument('--window-size=1920x1080') + chrome_options.add_argument( + 'user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36' + ) + return chrome_options + + def collect(self,whatsapp_session,message,targets=None): # find the names send the messages. + if targets is None: + if self._consume_json_data: + targets = self.debtor_data + else: + try: + raise ValueError('Targets Can Not Be None') + except ValueError as VE: + self._safe_exit_on_error(error=VE,session=whatsapp_session) + else: + for _target in targets: + target_data = [_target,targets[_target]] + message = self.get_message(message=message,*target_data) + searchbar = whatsapp_session.find_element_by_xpath(self._default_xpath_searchbar) + searchbar.click() + searchbar.send_keys(_target) + self._logit('Waiting for Data Send') + sleep(self.data_wait) + target = whatsapp_session.find_element_by_xpath(self._default_xpath_target_user % _target) + name = target.text + _number_of_debtors = len(targets) + if str(name) == str(_target): + target.click() + text_area = whatsapp_session.find_element_by_xpath(self._default_xpath_text_area) + text_area.click() + text_area.send_keys(message) + element = WebDriverWait(whatsapp_session, 20).until( + EC.presence_of_element_located((By.XPATH, self._default_xpath_send_button))) # raise exception if not found + send_button = whatsapp_session.find_element_by_xpath(self._default_xpath_send_button) + if send_button: + #send_button.click() + # detect os in later versions + #send_button.send_keys(Keys.RETURN) # linux + send_button.click() # windows + self._logit('Waiting for Data Send') + sleep(self.data_wait) + else: + raise Exception('Not Sent Element Not Found') + self.msg_count =+ 1 + self._logit('{}/{} People Have been Reminded about their outstanding balance!'.format(self.msg_count,_number_of_debtors)) + + return self._logit(f'Done!, all reminders have been sent... a total of {_number_of_debtors} Debtors have been notified.'),self._clock.append(time()),self.end(session=whatsapp_session) + + def start(self,targets,message='default'.upper()): + self._clock.append(time()) + session = self.authenticate() + return self._logit(self.collect(session,message,targets)) + + def end(self,session): + return self._logit(f'Collection Proccess took {round(self._clock[1] - self._clock[0],2)} Seconds!'),session.quit(),exit() + + def authenticate(self): #handle QR authentication + self._logit('Starting Authentication, allow a few seconds, get your phone ready for the QR code... (3-7 Seconds)') + sleep(r.randrange(3,7)) + browser = webdriver.Chrome(executable_path='/usr/local/bin/chromedriver') + if self.debug: + if browser: + if self.debug: + self._logit(f'Launched Browser Instance : {browser} at {str(datetime.fromtimestamp(time()))}') + browser.get(self.url_bin["whats_app"]) + radio_button_sign_in_xpath = "//input[@name='rememberMe']" # name=rememberMe going with CSS lots of 'input' tags + radio_button = browser.find_element_by_xpath(radio_button_sign_in_xpath).is_selected() + if radio_button is False: + radio_button.click() + self._logit('Enabled Persistant Login..') + else: + self._logit('Persistant Login Option Detected State: [ ON ]') + authenticated = False + # Explicit wait referenced below + _local_element_wait = 120 + # + self._logit(f'Scan QR Code Now! You have {_local_element_wait} Second(s)') + sleep(5) + try: + element = WebDriverWait(browser, _local_element_wait).until( + EC.presence_of_element_located((By.XPATH, self._default_xpath_authenticated))) + self._logit('Successfully Authenticated') + if self._kill_on_auth == int(1): + self._safe_exit_on_error(session=browser) + else: + return browser + except Exception as te: + self._safe_exit_on_error(error=te,session=browser) + + + + def connect(self,*K): # handle the initial connection and authentication into What's app return the login access + pass + + + def get_message(self,*target_data,message): # pretty done with this function for now + if message == 'default'.upper(): + message = "Beep Boop! I'm a robot... {debtor}, I'm here to remind you that you owe money to {debt_collector}. Please deposit {amount} {currency_format} to bank account {bank_account}. Thanks!" + else: + message = message + target = target_data[0] + amount = target_data[1] + return message.format(debtor=target,debt_collector=self._control_user["user"],amount=amount,currency_format=self._control_user["currency_format"],bank_account=self._control_user["bank_account"])