Continuing the experiments with the Web version of WhatsApp, the author tries to turn it into something resembling a Telegram bot. Everything here is free and needs no external services.
The only prerequisite is Python 3 with the packages listed in the imports below.
One note: this bot (code below) does not require AutoIt, since it does not send files in response to a request.
The gist of the idea. There is no dedicated server, so the bot has to run on a separate machine (a virtual machine, a Docker image, and so on). A robot runs in a loop there: it watches the group where all bot users are members and answers their requests. The robot does nothing clever, it just posts the text of the message back into the group with the "Echo:" prefix. Combined with a real handler for incoming messages, it can be turned into a proper auto-responder.
Now the bot's structure. It is implemented as a class called whatapp(). It has the following methods:
- startBrowser - starts the browser;
- finish - closes the browser;
- searchGroup - finds the group where messages will be processed;
- sendMessage - sends a message to the current group or to the current user's chat;
- searchLastMessageAndProcessMessages - finds the last processed message and then starts processing the new ones.
A few problems came up while building the bot. First, reading messages requires JavaScript disabled so that no new messages arrive and the DOM does not update, while sending messages requires JavaScript enabled. Second, the marker for the last processed message has to be stored somewhere; for simplicity the author saves it to a file. A database, with a separate marker per user, would be better, but the author did not want to complicate the bot's logic. Third, messages containing emoji had to be dropped, because those images would otherwise need separate handling.
One more remark: replies are sent to the same group where the requests are read. It would be better to find the chat of the user who made the request and reply only to them, but again the author chose not to complicate the bot's logic.
To be clear, this application is not positioned as a commercial project. For a small company where WhatsApp is the messenger of choice, it is a workable solution.
Here is the bot's code. Comments are inline throughout. One caveat: WhatsApp's aria-label values follow the interface language, so the selectors below are written against the English UI.
from selenium import webdriver
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.support.ui import WebDriverWait
from time import sleep
import argparse
from datetime import datetime
# Constants for Selenium
url = f"https://web.whatsapp.com/"
xpath = "//button[@data-testid='compose-btn-send']"
xpathAttach = "//div[@data-testid='conversation-clip']"
cssIdOfDocument = "[aria-label='Document']"
cssIdOfSendButton = "[aria-label='Send']"
xpathSearchField = "//div[@data-testid='chat-list-search']"
xpathFoundItem = "//span[contains(@class,'matched-text')]"
xpathInputLineForText = "//div[@data-testid='conversation-compose-box-input']"
xpathMessage = "//div[@role = 'row']"
xpathAuthorTime = "span[contains(@class,'_3FuDI')]"
xpathText1 = "span[contains(@class,'selectable-text')]"
xpathText2 = "span[contains(@class,'copyable-text')]"
commandEnableOrDisableJavaScript = "Emulation.setScriptExecutionDisabled"
# Constants for message process
messageIsOutgoing = "message-out"
messageIsText = "data-pre-plain-text"
messageClassOfBody = "selectable-text copyable-text"
messageBodyStart = "<span>"
messageBodyStop = "</span>"
messageIsPicture = "img"
messageWarrningAboutPicture = "I don't process emoticons!"
fileNameWithLastMessage = "lastmessage.txt"
class whatapp():
""" This class is bot for Whatsapp"""
def startBrowser(self):
options = webdriver.ChromeOptions()
# if you have the Mozilla Fifefox just write options = webdriver.FirefoxOptions()
options.add_argument('--allow-profiles-outside-user-dir')
options.add_argument('--enable-profile-shortcut-manager')
# Specify the path to where your python file is located. I suggest you create a separate folder for it
options.add_argument(r'user-data-dir=X:\\YYYYYY\\ZZZZZZ\\')
options.add_argument('--profile-directory=Profile 1')
options.add_argument('--profiling-flush=n')
options.add_argument('--enable-aggressive-domstorage-flushing')
# these options suppress any messages about bad SSL, certificates & etc — but only the last one works :(
options.add_argument('--ignore-certificate-errors-spki-list')
options.add_argument('--ignore-certificate-errors')
options.add_argument('--ignore-ssl-errors')
options.add_argument('log-level=3')
# INFO = 0,
# WARNING = 1,
# LOG_ERROR = 2,
# LOG_FATAL = 3.
# default is 0.
# We are starting a browser
self.driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
# it takes some time to load it
wait = WebDriverWait(self.driver, 30)
# go to there
self.driver.get(url)
# we are waiting for Whatsapp page to load
wait.until(EC.element_to_be_clickable((By.XPATH, xpathSearchField)))
def finish(self):
# close all
self.driver.quit()
def searchGroup(self, group_name):
# Find a group to process
# In the search field, enter the name of the group and click "Search".
self.driver.find_element(By.XPATH, xpathSearchField).send_keys(group_name)
self.driver.find_element(By.XPATH, xpathFoundItem).click()
sleep(5)
def sendMessage(self, message):
# Enable JavaSctipt
self.driver.execute_cdp_cmd(commandEnableOrDisableJavaScript, {'value': False})
# Find the input line and send the message text there
self.driver.find_element(By.XPATH, xpathInputLineForText).send_keys(message)
# now we look for the Send Button and click on it
self.driver.find_element(By.XPATH, xpath).click()
sleep(5)
def searchLastMessageAndProcessMessages(self, dt):
# Disable JavaSctipt
self.driver.execute_cdp_cmd(commandEnableOrDisableJavaScript, {'value': True})
# Select all messages
messages = self.driver.find_elements(By.XPATH, xpathMessage)
print(str(dt) + '. I found %s messages' % len(messages))
# Get the last processed message from file
f = open(fileNameWithLastMessage, "r")
last_message = f.read()
f.close()
need_process = False
# review all messages
for item in messages:
# get body of the message
line = str(item.get_attribute('innerHTML'))
# this is outgoing message... skip it
if (messageIsOutgoing in line):
continue
# this message without text... skip it
if not (messageIsText in line):
continue
# extract date, time and author from the message
index1 = line.index(messageIsText)
index2 = line.index("]", index1 + len(messageIsText))
index3 = line.index(":", index2)
author_datetime = line[index1 + len(messageIsText) + 2 : index3]
# extract text from the message
index1 = line.index(messageClassOfBody)
index2 = line.index(messageBodyStart, index1 + len(messageClassOfBody))
index3 = line.index(messageBodyStop, index2)
message = line[index2 + len(messageBodyStart): index3]
# create a complete message for comparison with the last processed message
full_message = author_datetime + " " + message
# if the message was last processed, we start processing by skipping this
if (full_message == last_message):
need_process = True
continue
# process messages. Send echo
if need_process:
# block a response for any pictures
if (messageIsPicture in message): ## smile
message = messageWarrningAboutPicture
self.sendMessage("Echo: " + message)
# Disable JavaSctipt
self.driver.execute_cdp_cmd(commandEnableOrDisableJavaScript, {'value': True})
# write last processed message to file
if not (author_datetime is None):
f = open(fileNameWithLastMessage, "w")
f.write(full_message)
f.close()
# Enable JavaSctipt
self.driver.execute_cdp_cmd(commandEnableOrDisableJavaScript, {'value': False})
def main(args):
# the main infinite loop. interrupt it with Ctrl+C
try:
while True:
wa = whatapp()
wa.startBrowser()
wa.searchGroup(args.group)
dt = datetime.now()
wa.searchLastMessageAndProcessMessages(dt);
wa.finish()
except KeyboardInterrupt:
wa.finish()
return
if __name__ == '__main__':
# we are parsing command line parameters
parser = argparse.ArgumentParser(description='Process messages by Whatsapp')
parser.add_argument('--group', help='Text for send', required=True)
args = parser.parse_args()
# start processing
main(args)
The bot is started as follows:
python whatsapp.py --group "Test"
Good luck.