В данной статье планирую поделиться с вами своей наработкой, которая позволяет создавать меню и кнопки вашего 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)


  1. randomsimplenumber
    30.07.2024 05:14

    А сами меню конструируют в SQLite browser? Или есть генератор меню для SQLite?


  1. kuzzdra
    30.07.2024 05:14

    А какой сценарий использования? Меню можно и из xml/json читать, зачем натягивать древовидную по сути структуру на плоскую таблицу?


  1. Dinxor
    30.07.2024 05:14

    К сожалению, автор почти год не появлялся на Хабре. Я нашёл эту статью в песочнице и отправил автору инвайт потому, что реализация показалась интересной. Сам давно использую нечто подобное, из плюсов - удобно править меню без изменения кода самого бота, стандартный FSM и бесконечная регистрация хендлеров бесят. Удобно раздавать права доступа, тем более юзеры уже в базе. Минусы - всё равно поверх приходится добавлять навигацию, не хватает гибкости, пока не придумал production-ready решения. Если получится что-то стОящее, обязательно напишу статью.


  1. Maximus2017
    30.07.2024 05:14

    Подскажи, как данные в таблицу заводишь? Я через DBeaver пробую через импорт Excel. И у меня все emoji слетают.