Как стать автором
Обновить

Простейший бот «на движке» WEB WhatsApp

Уровень сложности Средний
Время на прочтение 6 мин
Количество просмотров 6.2K

Доброго времени суток всем!

Продолжу изгаляться над Web версией Whatsapp и попробую превратить его в подобие телеграмм-бота. Тем более, что все это бесплатно и не требует никаких внешних сервисов.

Я не буду повторять все действия, которые нужны для для запуска python c необходимыми пакетами, они описаны в моей предыдущей статье.

Только упомяну, что в данном боте (его код будет ниже) не требуется подключение autoit, так как он не предусматривает отправку файлов в ответ на запрос.

Теперь опишу суть задумки. Так как нет специализированного сервера, то придётся использовать выделенный компьютер (виртуальную машину, docker-образ & etc). На нём в цикле запускать робота, который будет просматривать группу, где подключены все пользователи бота, и отвечать на их запросы. Данный робот ничего умного не умеет, кроме как отправить назад, в группу текст сообщения с префиксом "Echo:". Но с учетом предыдущего поста, думаю, что можно превратить его в полноценного "ответчика".

А сейчас, подробнее по структуре бота. Он реализован в классе, который называется whatapp(). В нём есть следующие методы:

  • startBrowser - запускает браузер;

  • finish - закрывает браузер;

  • searchGroup - ищет группу, в которой будут обрабатывать сообщения;

  • sendMessage - отсылает сообщение в текущую группу или в текущий чат пользователя;

  • searchLastMessageAndProcessMessages - осуществляет поиск последнего обработанного сообщения и затем приступает к обработке новых.

Отмечу встреченные проблемы при построении бота. Первое, для просмотра сообщений нужно блокировать работу JavaScript, чтобы не появлялись новые сообщения и не обновлялся DOM, а для отправки сообщений необходимо разрешить JavaScript. Второе, нужно где-то хранить маркер для последнего обработанного сообщения, я для простоты сохраняю его в файле. Конечно же лучше хранить эти маркеры в базе данных и для каждого пользователя, но я не стал загромождать логику бота. Третье, пришлось отбрасывать сообщения, которые содержат смайлики, так в этом случае пришлось бы еще как-то обрабатывать эти картинки.

И еще одно замечание, ответы отправляются в ту же самую группу, где просматриваются запросы. Лучше бы было поискать чат запросившего пользователя и отправить ответ только ему, но я, опять же, не стал загромождать логику работы бота.

Подчеркну, что я не рассматриваю данное приложение как коммерческий проект, но для небольшой компании, в которой фаворитом среди мессенджеров является Whatsapp, это вполне приемлемое решение.

Теперь текст бота. Комментарии, как обычно, по ходу текста и на двух языках.

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


# Константы для Selenium
# 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='Документ']"
cssIdOfSendButton = "[aria-label='Отправить']"
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():
    """ Это класс бота для Whatsapp
       This class is bot for Whatsapp"""


    def startBrowser(self):
        options = webdriver.ChromeOptions()
# если у вас другой браузер, например FireFox, мы просто пишем options = webdriver.FirefoxOptions() .
# 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')
# УКАЖИТЕ ПУТЬ ГДЕ ЛЕЖИТ ВАШ python ФАЙЛ. Советую создать отдельную папку для него
# 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')

# эти опции нужны чтобы подавить любые сообщения об ошибках  SSL, сертификатов и т.п. Но работает только последняя :(
# these options need to disabled any messages about bad ssl, certification & etc
        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)
# ждём загрузки страницы Whatsapp
# 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):
# Разрешаем JavaSctipt
# 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):
# Запрещаем JavaSctipt
# 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))
# Получим последнеe обработанное сообщение из файла
# 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)
# Запрещаем JavaSctipt
# 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()
# Разрешаем JavaSctipt
# Enable    JavaSctipt
        self.driver.execute_cdp_cmd(commandEnableOrDisableJavaScript, {'value': False})


def main(args):
# основной бесконечный цикл. Прервать его - Ctrl+C
# 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)

  

Запуск бота осуществляется следующим образом

python whatsapp.py --group "Тест"

Всем удачи!

Теги:
Хабы:
+3
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

Data Scientist
73 вакансии
Python разработчик
129 вакансий

Ближайшие события

DI CONF SMM — большая конференция по соцсетям в России
Дата 2 марта
Время 09:30 – 18:00
Место
Краснодар Онлайн
Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн