Создание ботов — довольно заезжанная тема, но все уроки, статьи и различного рода документация дают информацию только о том, как построить бота в один уровень без возможности создания древа из различных всплывающих меню (клавиатур).
А это нужно для создания:
-
Сложных ботов с несколькими уровнями «глубины» (различные меню/клавиатуры)
-
Ботов, созданных одновременно для групповых чатов и для лички сообщества
-
Ботов, с повторяющимися ключевыми командами в различных меню, которые необходимо разделять

Можно сделать многоуровневого бота на Python сразу (без, например, библиотеки threading и без базы данных), но он будет адекватно работать только для одного пользователя в текущий момент времени, потому что одна клавиатура и текущее положение пользователя будет сохраняться в MainThread (основном треде) для всех.
Предварительная подготовка
Устанавливаем MySQL на необходимую машину:
https://dev.mysql.com/downloads/mysql/
Или через терминал (для Linux):
sudo apt install mysql-server
Если у Вас не установлены необходимые библиотеки — то устанавливаем их через терминал:
pip3 install vk_api pip3 install pymysql
В базе данных нам предварительно нужно создать базу данных в MySQL с названием vktest с таблицей user и полями — iduser и position. Я создаю БД с кодировкой utf8mb4:
CREATE SCHEMA vktest DEFAULT CHARACTER SET utf8mb4 ; CREATE TABLE `vktest`.`user` ( `iduser` INT UNSIGNED NOT NULL, `position` TINYINT UNSIGNED NULL, PRIMARY KEY (`iduser`));
Базовый алгоритм

Основная идея алгоритма в том, что основной тред будет занят слушанием longpoll, который при появлении нового сообщения будет создавать новый тред. В этот новый тред, как аргументы, будут отсылаться айди пользователя и текст сообщения и далее обрабатываться.
В новосозданном треде бот сразу берет текущее положение пользователя в БД, в зависимости от этого положения происходит обработка сообщения и после обновляется положение пользователя в БД на новое и выполняются нужные инструкции.
Для начала представим этот алгоритм для основной функции, которая и будет создавать эти треды (код составлен для сообщений в личку сообщества):
import vk_api from vk_api import VkUpload from vk_api.utils import get_random_id from vk_api.bot_longpoll import VkBotLongPoll, VkBotEventType import threading import requests import random import pymysql import pymysql.cursors if __name__ == '__main__': while True: session = requests.Session() vk_session = vk_api.VkApi(token="%Токен сообщества VK%") vk = vk_session.get_api() upload = VkUpload(vk_session) longpoll = VkBotLongPoll(vk_session, "%ID сообщества VK%") try: for event in longpoll.listen(): if event.type == VkBotEventType.MESSAGE_NEW and event.from_user: threading.Thread(target=processing_message, args=(event.obj.from_id, event.obj.text)).start() except Exception: pass
Весь код в основной функции завернут в while True из-за того, что раньше каждую ночь (возможно также и сейчас) примерно в 4:30 по МСК переставал отвечать VK на запросы и бот падал (видимо перезагружали серверы).
Основная идея тренировочного бота — это сделать три меню, где Основное меню 1 будет иметь возможность попасть в Меню 2 и в Меню 3, а они, в свою очередь, могли вернуться обратно в Основное меню 1:

В формате меню в самом ВКонтакте хочется представить это так:

У нас 3 клавиатуры, которые нужно создать в виде файлов с расширением .json в папке с main.py. Первый файл keyboard_main.json будет с кодом:
keyboard_main.json
{ "one_time": false, "buttons": [ [{ "action": { "type": "text", "label": "Цитаты Дурова" }, "color": "default" }], [ { "action": { "type": "text", "label": "Цитаты Цукерберга" }, "color": "default" }] ] }
Второй файл keyboard_durov.json с кодом:
keyboard_durov.json
{ "one_time": false, "buttons": [ [{ "action": { "type": "text", "label": "Хочу ещё Дурова" }, "color": "positive" }], [ { "action": { "type": "text", "label": "Выйти в главное меню" }, "color": "default" }] ] }
Третий, практически идентичный — keyboard_zuckerberg.json:
keyboard_zuckerberg.json
{ "one_time": false, "buttons": [ [{ "action": { "type": "text", "label": "Хочу ещё Цукерберга" }, "color": "positive" }], [ { "action": { "type": "text", "label": "Выйти в главное меню" }, "color": "default" }] ] }
Значение target у нас processing_message, поэтому создаём функцию в main.py с именем processing_message и c аргументами в виде id юзера и его сообщения:
processing_message
def processing_message(id_user, message_text): number_position = take_position(id_user) if number_position == 0: send_message(id_user, "keyboard_main.json", "Тебя приветствует бот!") add_new_line(id_user) elif number_position == 1: if message_text == "Цитаты Дурова": update_position(id_user, "2") send_message(id_user, "keyboard_durov.json", durov_quote()) elif message_text == "Цитаты Цукерберга": update_position(id_user, "3") send_message(id_user, "keyboard_zuckerberg.json", zuckerberg_quote()) else: send_message(id_user, "keyboard_main.json", "Непонятная команда") elif number_position == 2: if message_text == "Хочу ещё Дурова": send_message(id_user, "keyboard_durov.json", durov_quote()) elif message_text == "Выйти в главное меню": update_position(id_user, "1") send_message(id_user, "keyboard_main.json", "Мы в главном меню") else: send_message(id_user, "keyboard_durov.json", "Непонятная команда") elif number_position == 3: if message_text == "Хочу ещё Цукерберга": send_message(id_user, "keyboard_zuckerberg.json", zuckerberg_quote()) elif message_text == "Выйти в главное меню": update_position(id_user, "1") send_message(id_user, "keyboard_main.json", "Мы в главном меню") else: send_message(id_user, "keyboard_zuckerberg.json", "Непонятная команда") else: send_message(id_user, "keyboard_main.json", "Произошла какая-то ошибка")
Весь алгоритм этой функции построен на том, что мы сразу берем текущую позицию пользователя и в зависимости от неё обрабатываем сообщение пользователя. Если number_position — это 0 (функция take_position(id_user) вернула 0, то есть в базе данных она не нашла пользователя), то она сразу добавляет его в БД с позицией 1 через функцию add_new_line(id_user), перемещая таким образом его в главное меню и открывая ему клавиатуру keyboard_main.json.
Если number_position от 1 до 3 — то бот выполняет код в соответствующем для него ветвлении if-elif-else.
Функция take_position(id_user) выглядит таким образом:
def take_position(id_user): connection = get_connection() try: with connection.cursor() as cursor: sql = "SELECT position FROM user WHERE iduser = %s" cursor.execute(sql, (id_user)) line = cursor.fetchone() if line is None: return_count = 0 else: return_count = line["position"] finally: connection.close() return return_count
В свою очередь add_new_line(id_user) выглядит так:
def add_new_line(id_user): connection = get_connection() try: with connection.cursor() as cursor: sql = "INSERT INTO user (iduser, position) VALUES (%s, %s)" cursor.execute(sql, (id_user, "1")) connection.commit() finally: connection.close() return
Когда пользователь имеет свою строку в БД — он уже может приступать к взаимодействию с ботом. И обновление его положения — это функция update_position(id_user, new_position), которая выглядит таким образом:
def update_position(id_user, new_position): connection = get_connection() try: with connection.cursor() as cursor: sql = "UPDATE user SET position = %s WHERE iduser = %s" cursor.execute(sql, (new_position, id_user)) connection.commit() finally: connection.close() return
Эти три функции не будут работать без основной функции для БД (которую необходимо добавить практически в самое начало, сразу после импорта библиотек) и вписать ваши данные от БД. Я изменяю только название БД и кодировку на те, которые мы создали в самом начале:
def get_connection(): connection = pymysql.connect(host="%name_host%", user="%name_user%", password="%password_user%", db="vktest", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor) return connection
И одна из самых важных функций — отправка сообщения пользователю. Я вывел её в отдельную функцию send_message, так как это нерационально возить такую махину постоянно по всему коду:
def send_message(id_user, id_keyboard, message_text): try: vk.messages.send( user_id=id_user, random_id=get_random_id(), keyboard=open(id_keyboard, 'r', encoding='UTF-8').read(), message=message_text) except: print("Ошибка отправки сообщения у id" + id_user)
Ничего не забыли? Ах да, нам же нужны крутые цитаты Дурова и Цукерберга:
durov_quote() и zuckerberg_quote()
def durov_quote(): durov = ['Лучшее решение из возможных — самое простое. И наоборот.', 'Что такое университет? Это же раздробленная структура с удельными княжествами.', 'Коммуникация переоценена. Час одиночества продуктивнее недели разговоров.', 'Проблемы — это спрятанные решения.', 'Врать вредно для духовной целостности.'] return random.choice(durov) def zuckerberg_quote(): zuckerberg = ['В мире, который меняется очень быстро, единственная стратегия, которая гарантированно ' 'провальна — не рисковать.', 'Двигайтесь быстро и разрушайте препятствия. Если вы ничего не разрушаете, ' 'Вы движетесь недостаточно быстро.', 'Вопрос не в том, что мы хотим знать о человеке. Вопрос стоит так:' '«Что люди хотят рассказать о себе?»', 'Люди могут быть очень умными или иметь отличные профессиональные навыки, ' 'но если они действительно не верят в свое дело, они не будут по-настоящему работать.', 'Вопрос, который я задаю себе почти каждый день: сделал ли я самую важную вещь, которую ' 'я мог бы сделать? Если я не чувствую, что я работаю над самой важной проблемой, где я ' 'могу помочь, я не буду чувствовать, что хорошо провожу свое время'] return random.choice(zuckerberg)
Для них мы использовали встроенные списки Python и подключили библиотеку random.
И весь код main.py (целиком) под спойлером для удобства:
main.py
import vk_api from vk_api import VkUpload from vk_api.utils import get_random_id from vk_api.bot_longpoll import VkBotLongPoll, VkBotEventType import threading import random import pymysql import pymysql.cursors import requests def get_connection(): connection = pymysql.connect(host="%name_host%", user="%name_user%", password="%password_user%", db="vktest", charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor) return connection def send_message(id_user, id_keyboard, message_text): try: vk.messages.send( user_id=id_user, random_id=get_random_id(), keyboard=open(id_keyboard, 'r', encoding='UTF-8').read(), message=message_text) except: print("Ошибка отправки сообщения у id" + id_user) def add_new_line(id_user): connection = get_connection() try: with connection.cursor() as cursor: sql = "INSERT INTO user (iduser, position) VALUES (%s, %s)" cursor.execute(sql, (id_user, "1")) connection.commit() finally: connection.close() return def take_position(id_user): connection = get_connection() try: with connection.cursor() as cursor: sql = "SELECT position FROM user WHERE iduser = %s" cursor.execute(sql, (id_user)) line = cursor.fetchone() if line is None: return_count = 0 else: return_count = line["position"] finally: connection.close() return return_count def update_position(id_user, new_position): connection = get_connection() try: with connection.cursor() as cursor: sql = "UPDATE user SET position = %s WHERE iduser = %s" cursor.execute(sql, (new_position, id_user)) connection.commit() finally: connection.close() return def durov_quote(): durov = ['Лучшее решение из возможных — самое простое. И наоборот.', 'Что такое университет? Это же раздробленная структура с удельными княжествами.', 'Коммуникация переоценена. Час одиночества продуктивнее недели разговоров.', 'Проблемы — это спрятанные решения.', 'Врать вредно для духовной целостности.'] return random.choice(durov) def zuckerberg_quote(): zuckerberg = ['В мире, который меняется очень быстро, единственная стратегия, которая гарантированно ' 'провальна — не рисковать.', 'Двигайтесь быстро и разрушайте препятствия. Если вы ничего не разрушаете, ' 'Вы движетесь недостаточно быстро.', 'Вопрос не в том, что мы хотим знать о человеке. Вопрос стоит так:' '«Что люди хотят рассказать о себе?»', 'Люди могут быть очень умными или иметь отличные профессиональные навыки, ' 'но если они действительно не верят в свое дело, они не будут по-настоящему работать.', 'Вопрос, который я задаю себе почти каждый день: сделал ли я самую важную вещь, которую ' 'я мог бы сделать? Если я не чувствую, что я работаю над самой важной проблемой, где я ' 'могу помочь, я не буду чувствовать, что хорошо провожу свое время'] return random.choice(zuckerberg) def processing_message(id_user, message_text): number_position = take_position(id_user) if number_position == 0: send_message(id_user, "keyboard_main.json", "Тебя приветствует бот!") add_new_line(id_user) elif number_position == 1: if message_text == "Цитаты Дурова": update_position(id_user, "2") send_message(id_user, "keyboard_durov.json", durov_quote()) elif message_text == "Цитаты Цукерберга": update_position(id_user, "3") send_message(id_user, "keyboard_zuckerberg.json", zuckerberg_quote()) else: send_message(id_user, "keyboard_main.json", "Непонятная команда") elif number_position == 2: if message_text == "Хочу ещё Дурова": send_message(id_user, "keyboard_durov.json", durov_quote()) elif message_text == "Выйти в главное меню": update_position(id_user, "1") send_message(id_user, "keyboard_main.json", "Мы в главном меню") else: send_message(id_user, "keyboard_durov.json", "Непонятная команда") elif number_position == 3: if message_text == "Хочу ещё Цукерберга": send_message(id_user, "keyboard_zuckerberg.json", zuckerberg_quote()) elif message_text == "Выйти в главное меню": update_position(id_user, "1") send_message(id_user, "keyboard_main.json", "Мы в главном меню") else: send_message(id_user, "keyboard_zuckerberg.json", "Непонятная команда") else: send_message(id_user, "keyboard_main.json", "Произошла какая-то ошибка") if __name__ == '__main__': while True: session = requests.Session() vk_session = vk_api.VkApi(token="%Токен сообщества VK%") vk = vk_session.get_api() upload = VkUpload(vk_session) longpoll = VkBotLongPoll(vk_session, "%ID сообщества VK%") try: for event in longpoll.listen(): if event.type == VkBotEventType.MESSAGE_NEW and event.from_user: threading.Thread(target=processing_message, args=(event.obj.from_id, event.obj.text)).start() except Exception: pass
Заключение
Этой статьей я поделился с массами адекватным, как мне кажется, решением для создания многоуровневого бота. Надеюсь, это защитит многих на старте от решений в виде цикла longpoll в цикле longpoll и в ещё одном longpoll, из которых довольно трудно выйти. Всем удачи.
ссылка на оригинал статьи https://habr.com/ru/post/648591/
Добавить комментарий