Contact Us

The Simplest Bot Running on the WhatsApp Web Engine

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.