В данной статье планирую поделиться с вами своей наработкой, которая позволяет создавать меню и кнопки вашего Telegram бота на основе данных хранящихся в БД.
Реализовывать все это будем на Python и нам потребуются библиотеки:
sqlite3 - для работы с БД (устанавливать не нужно, поставляется в коробке с Питоном)
pyTelegramBotAPI - для создания Telegram бота (предварительно необходимо установить)
Представьте, что мы имеем файл базы данных "database.db" с таблицей, которая называется "create_menu". Эта таблица хранит следующую информацию:
type_menu |
order_num |
btn_name |
btn_callback |
buy |
1 |
?Яблоки |
apple |
buy |
2 |
?Лимоны |
lemon |
buy |
3 |
?Бананы |
banana |
buy |
4 |
?Назад |
back |
help |
1 |
?Назад |
back |
main |
1 |
?Купить |
buy |
main |
2 |
?Продать |
sell |
main |
3 |
?Помощь |
help |
sell |
1 |
?Томаты |
tomato |
sell |
2 |
?Кокосы |
coconut |
sell |
3 |
?Манго |
mango |
sell |
4 |
?Назад |
back |
type_menu - название меню, к которому будет относиться данная кнопка
order_num - порядковый номер кнопки в меню (сверху вниз)
btn_name - текст, который будет отображаться на кнопке
btn_callback - данные, которые будут возвращены при нажатии на кнопку. Их будет отлавливать обработчик событий.
Ну что, теперь поехали!
Создадим класс CreateMenu и сразу инициализируем в нём путь к нашему файлу базы данных.
import os
import sqlite3
from telebot import types
class CreateMenu:
def __init__(self):
'''Конструктор класса. Определяет файл базы данных'''
self.__db_path = os.getcwd()
self.db_name = os.path.join(self.__db_path, 'database.db')
По умолчанию считаем, что файл БД находиться в одном каталоге с файлом программы, поэтому строим путь к нему используя os.getcwd() и os.path.join().
Теперь сделаем внутренний метод __connect() который будет отвечать за подключение к нашей БД с использованием библиотеки sqlite3.
def __connect(self):
'''Функция подключения к базе данных'''
connect = sqlite3.connect(self.db_name)
return connect
Так же сделаем внутренний метод __select_button() который будет бегать в БД с SQL запросом и возвращать нам словарь (dict), где ключ (key) = название кнопки и значение (value) = callback data этой кнопки. Что бы этот метод не тащил абы чего, он будет принимать аргумент type_menu. Это позволит нам набирать кнопки под конкретное меню.
def __select_button(self, type_menu: str) -> dict:
'''В качестве аргумента принимает "тип меню". Возвращает словарь где ключ = текст кнопки, значение = callbackdata кнопки'''
with self.__connect() as connect:
cursor = connect.cursor()
sql = """SELECT btn_name, btn_callback FROM create_menu WHERE type_menu = (?) ORDER BY order_num"""
select_db = cursor.execute(sql, (type_menu,))
result = dict()
for btn_name, btn_callback in select_db.fetchall():
result[btn_name] = btn_callback
return result
Думаю стоит объяснить, что за магия тут твориться.-
Подключаемся к БД (используя ранее заготовленный __connect())
-
Выполняем SQL запрос, который дословно можно перевести :
"Покажи мне "название кнопки" и "её callback" из таблицы create_menu где тип меню равен type_menu и отсортируй все это по order_num в порядке возрастания.
Создаем словарик в который будем записывать результат SQL запроса.
Пробегаемся в цикле по результату SQL запроса и добавляем новые записи в словарь.
-
Возвращаем из функции словарь.
Например если мы передадим в качестве аргумента "main" функция вернёт словарь: {'?Купить': 'buy', '?Продать': 'sell', '?Помощь': 'help'})
Ну и давайте напишем единственный метод в классе, который будет вызываться программистом. Назовем его create_menu() и как вы догадались он будет создавать меню.
def create_menu(self, type_menu: str) -> types.InlineKeyboardMarkup:
'''Создаём меню для TG бота'''
markup = types.InlineKeyboardMarkup()
btn_list = self.__select_button(type_menu)
for element in btn_list.items():
btn = types.InlineKeyboardButton(text= element[0], callback_data= element[1])
markup.add(btn)
return markup
Метод в качестве аргумента принимает тип меню (type_menu), которое мы хотим создать. Далее он с этим аргументам дёргает выше рассмотренный внутренний метод __select_button() и получает в свое распоряжение словарик из которого будет лепить меню.
По классике жанра, пробегает словарик в цикле и добавляет в меню (markup) кнопки, где текст кнопки - ключ словаря, а её обратный данные - значение из словаря.
Итоговый код выглядит следующим образом:
import os
import sqlite3
from telebot import types
class CreateMenu:
def __init__(self):
'''Конструктор класса. Определяет файл базы данных'''
self.__db_path = os.getcwd()
self.db_name = os.path.join(self.__db_path, 'database.db')
def __connect(self):
'''Функция подключения к базе данных'''
connect = sqlite3.connect(self.db_name)
return connect
def __select_button(self, type_menu: str) -> dict:
'''В качестве аргумента принимает "тип меню". Возвращает словарь где ключ = текст кнопки, значение = callbackdata кнопки'''
with self.__connect() as connect:
cursor = connect.cursor()
sql = """SELECT btn_name, btn_callback FROM create_menu WHERE type_menu = (?) ORDER BY order_num"""
select_db = cursor.execute(sql, (type_menu,))
result = dict()
for btn_name, btn_callback in select_db.fetchall():
result[btn_name] = btn_callback
return result
def create_menu(self, type_menu: str) -> types.InlineKeyboardMarkup:
'''Создаём меню для TG бота'''
markup = types.InlineKeyboardMarkup()
btn_list = self.__select_button(type_menu)
for element in btn_list.items():
btn = types.InlineKeyboardButton(text= element[0], callback_data= element[1])
markup.add(btn)
return markup
Теперь мы можем импортировать написанный нами класс в свой проект. Создать экземпляр класса CreateMenu и использовать его метод create_menu() для создания разного рода меню.
Примечание класс CreateMenu описывался мной в файле с именем db.py
Пример использования:
import telebot
from db import CreateMenu
bot = telebot.TeleBot("ТОКЕН ВАШЕГО БОТА")
cm = CreateMenu()
@bot.message_handler(commands=["start"])
def start(message):
bot.send_message(message.chat.id, "Добро пожаловать", reply_markup= cm.create_menu('main'))
@bot.callback_query_handler(func=lambda call: True)
def callback_inline(call):
if call.data == 'buy':
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Что покупаем?", reply_markup= cm.create_menu('buy'))
if call.data == 'sell':
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Что продаём?", reply_markup= cm.create_menu('sell'))
if call.data == 'back':
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Добро пожаловать", reply_markup= cm.create_menu('main'))
if call.data == 'help':
bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Ничем не могу помочь тебе", reply_markup= cm.create_menu('help'))
if __name__ == "__main__":
bot.polling(none_stop=True)
Исходник проекта и файл базы данных из примера, вы найдёте у меня на GitHub
Надеюсь данный материал был полезен для вас!
Спасибо за внимание!
Комментарии (4)
kuzzdra
30.07.2024 05:14А какой сценарий использования? Меню можно и из xml/json читать, зачем натягивать древовидную по сути структуру на плоскую таблицу?
Dinxor
30.07.2024 05:14К сожалению, автор почти год не появлялся на Хабре. Я нашёл эту статью в песочнице и отправил автору инвайт потому, что реализация показалась интересной. Сам давно использую нечто подобное, из плюсов - удобно править меню без изменения кода самого бота, стандартный FSM и бесконечная регистрация хендлеров бесят. Удобно раздавать права доступа, тем более юзеры уже в базе. Минусы - всё равно поверх приходится добавлять навигацию, не хватает гибкости, пока не придумал production-ready решения. Если получится что-то стОящее, обязательно напишу статью.
Maximus2017
30.07.2024 05:14Подскажи, как данные в таблицу заводишь? Я через DBeaver пробую через импорт Excel. И у меня все emoji слетают.
randomsimplenumber
А сами меню конструируют в SQLite browser? Или есть генератор меню для SQLite?