Сообщить об ошибке.

Меню из кнопок, модуль python-telegram-bot

Создание встроенные клавиатуры для Telegram-бота

Внимание! Пакеты python-telegram-bot версии 13.x будут придерживаться многопоточной парадигмы программирования (*на данный момент актуальна версия 13.15). Пакеты версий 20.x и новее предоставляют чистый асинхронный Python интерфейс для Telegram Bot API. Дополнительно смотрите основные изменения в пакете python-telegram-bot версии 20.x.

Всякий раз, когда бот отправляет сообщение, он может передать специальную клавиатуру с предопределенными параметрами ответа. Приложения Telegram, которые получают сообщение, будут отображать эту клавиатуру для пользователя. Нажатие любой из кнопок немедленно отправит соответствующую команду. Таким образом можно значительно упростить взаимодействие пользователя с ботом.

Содержание.


Встроенные клавиатуры Telegramm в сообщения бота.

Бывают случаи, когда нужно что-либо сделать, не отправляя никаких сообщений в чат. Например, когда пользователь меняет настройки или просматривает результаты поиска. В таких случаях можно использовать встроенные InlineKeyboardButton клавиатуры, которые интегрированы непосредственно в сообщения, которым они принадлежат.

В отличие от настраиваемых клавиатур KeyboardButtons, которые посылают текст кнопки в качестве ответа, нажатие кнопок на встроенных клавиатурах InlineKeyboardButton не приводит к отправке сообщений в чат. Вместо этого встроенные клавиатуры поддерживают кнопки, которые работают за кулисами: кнопки обратного вызова, кнопки с URL и переключение на встроенные кнопки.

Когда используются кнопки обратного вызова (с аргументом callback_data), бот может обновлять свои существующие сообщения или клавиатуры. При нажатии на такую кнопку, бот просто получает соответствующий запрос. Получив запрос, бот может отображать результат в уведомлении в верхней части экрана чата или в предупреждении.

Классы KeyboardButton и InlineKeyboardButton.

Данные классы определяют атрибуты и методы, одноименные с названиями аргументов.

KeyboardButton(text, request_contact=None, request_location=None, request_poll=None, **_kwargs):

Объект KeyboardButton представляет собой одну кнопку клавиатуры для ответа текстом text, который отображается на кнопке. Необязательные аргументы исключают друг друга. Импортируется из основного модуля telegram.KeyboardButton.

Значение и поведение аргументов KeyboardButton:

  • text (str) - текст кнопки. Если ни одно из дополнительных полей не используется, оно будет отправлено боту в виде сообщения при нажатии кнопки.
  • request_contact (bool, необязательно) - если True, то при нажатии будет отправлен телефонный номер пользователя, как контакт. Доступно только в приватных чатах.
  • request_location (bool, необязательный) - если True, то при нажатии будет отправлено текущее местоположение пользователя. Доступно только в приватных чатах.
  • request_poll (KeyboardButtonPollType, необязательно) - если указано, то при нажатии кнопки пользователю будет предложено создать опрос и отправить его боту. Доступно только в приватных чатах.
  • **_kwargs (dict) - произвольные ключевые аргументы.
InlineKeyboardButton(text, url=None, callback_data=None, switch_inline_query=None, switch_inline_query_current_chat=None, callback_game=None, pay=None, login_url=None, **_kwargs)

Объект InlineKeyboardButton представляет одну кнопку встроенной клавиатуры. Допускается использовать ровно одно из необязательных полей. Импортируется из основного модуля telegram.InlineKeyboardButton.

Значение и поведение аргументов InlineKeyboardButton:

  • text (str) - текст кнопки. Если ни одно из дополнительных полей не используется, оно будет отправлено боту в виде сообщения при нажатии кнопки.
  • url (str) - HTTP или tg://url, который открывается при нажатии кнопки.
  • login_url (telegram.LoginUrl, необязательно) - URL-адрес HTTP, используемый для автоматической авторизации пользователя. Может использоваться как замена виджета входа в Telegram.
  • callback_data (str, необязательно) - данные, которые будут отправлены в запросе обратного вызова боту при нажатии кнопки, UTF-8 1-64 байта.
  • switch_inline_query (str, необязательно) - если установлено, то нажатие кнопки предложит пользователю выбрать один из своих чатов, открыть этот чат и вставить логин бота и указанный встроенный запрос в поле ввода. Может быть пустым, и в этом случае будет вставлено только логин бота. Это дает пользователям простой способ начать использовать вашего бота во встроенном режиме, в то время как они находятся с ним в приватном чате. Особенно полезно в сочетании с действиями switch_pm* - в этом случае пользователь автоматически вернется в чат, из которого он переключился, пропуская экран выбора чата.
  • switch_inline_query_current_chat (str, необязательно) - если установлено, то нажатие кнопки вставит логин бота и указанный встроенный запрос в поле ввода текущего чата. Может быть пустым, и в этом случае будет вставлено только логин бота. Это предлагает пользователю быстрый способ открыть вашего бота во встроенном режиме в том же чате - удобно для выбора чего-либо из нескольких вариантов.
  • callback_game (telegram.CallbackGame, необязательно) - описание игры, которая будет запускаться при нажатии кнопки пользователем. Кнопка этого типа всегда должна быть первой кнопкой в ​​первом ряду.
  • pay (bool, необязательно) - укажите True, чтобы отправить кнопку Pay. Кнопка этого типа всегда должна быть первой кнопкой в ​​первом ряду.
  • **_kwargs (dict) - произвольные ключевые аргументы.

Алгоритм построения и отправки кнопок в Telegram чат.

Для создания макета кнопок со столбцами n_cols из списка кнопок необходимо создать функцию build_menu(), которая будет шаблоном для построения кнопок:

def build_menu(buttons, n_cols,
               header_buttons=None,
               footer_buttons=None):
    menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
    if header_buttons:
        menu.insert(0, [header_buttons])
    if footer_buttons:
        menu.append([footer_buttons])
    return menu

В коде выше определены списки header_buttons и footer_buttons, их можно использовать чтобы поместить кнопки в первую или последнюю строку соответственно.

В приведенном ниже фрагменте кода нужно заменить ... соответствующим значением аргумента callback_data - это строка (UTF-8 1-64 байта) с данными, отправляемые боту в ответном запросе при нажатии кнопки. Если будете использовать кнопки KeyboardButtons для создания списка кнопок button_list, то для построения передаваемой в чат клавиатуры из кнопок используйте ReplyKeyboardMarkup вместо InlineKeyboardMarkup.

# список кнопок
button_list = [
    InlineKeyboardButton("col1", callback_data=...),
    InlineKeyboardButton("col2", callback_data=...),
    InlineKeyboardButton("row 2", callback_data=...)
]

# сборка клавиатуры из кнопок `InlineKeyboardButton`
reply_markup = InlineKeyboardMarkup(build_menu(button_list, n_cols=2))
# отправка клавиатуры в чат для ВЕРСИИ 13.x
bot.send_message(chat_id=chat_id, text="Меню из двух столбцов", reply_markup=reply_markup)
# или
# отправка клавиатуры в чат для ВЕРСИИ 20.x
# await bot.send_message(chat_id=chat_id, text="Меню из двух столбцов", reply_markup=reply_markup)

Или, если нужна динамическая версия, используйте генератор списка для динамического создания button_list из списка строк:

# построение простых кнопок для ответа 
# текстом, расположенным на кнопках
some_strings = ["col1", "col2", "row2"]
button_list = [[KeyboardButton(ss)] for ss in some_strings]
# сборка клавиатуры из кнопок `KeyboardButton`
reply_markup = ReplyKeyboardMarkup(build_menu(button_list, n_cols=2))
# отправка клавиатуры в чат для ВЕРСИИ 13.x
bot.send_message(chat_id=chat_id, text="Меню из двух столбцов", reply_markup=reply_markup)
# или
# отправка клавиатуры в чат для ВЕРСИИ 20.x
# await bot.send_message(chat_id=chat_id, text="Меню из двух столбцов", reply_markup=reply_markup)

Это особенно полезно, если поместить внутрь вспомогательного метода, такого как get_data_buttons, для работы с динамическими данными и обновления меню в соответствии с вводом пользователя.

Чтобы обработать callback_data, необходимо подключить обработчик CallbackQueryHandler.

Обработчик сообщений CallbackQueryHandler.

Обработчик сообщений CallbackQueryHandler определяет атрибуты и методы, одноименные с названиями аргументов. Обработчик CallbackQueryHandler импортируется из модуля расширения telegram.ext.

CallbackQueryHandler(callback, pattern=None, block=True):

Объект CallbackQueryHandler представляет собой обработчик запросов обратного вызова Telegram. Может использовать дополнительную фильтрацию на основе регулярных выражений модуля re.

Значение и поведение аргументов InlineKeyboardButton:

  • callback - Функция обратного вызова для этого обработчика. Будет вызываться, когда сообщение должно быть обработано этим обработчиком.
  • pattern=None (str, необязательно) - шаблон регулярного выражения. Если не None, то для поиска совпадений в telegram.CallbackQuery.data (должно ли сообщение обрабатываться этим обработчиком) будет использоваться функция re.match().
  • run_async=False (bool) - (удален в версии 20.x) определяет, будет ли обратный вызов выполняться асинхронно.
  • block=True (bool) - (новое в версии 20.x) определяет, следует ли ожидать возвращаемого значения обратного вызова перед обработкой следующего обработчика в telegram.ext.Application.process_update().

Базовый пример, использующий встроенную клавиатуру.

Примечание Этот пример будет работать в версии пакета 13.x. Для асинхронной версии пакета 20.x необходимые изменения прокомментированы в коде.

Дополнительно смотрите обзорный материал, подраздел "Асинхронный модуль расширения telegram.ext (версия 20.x)".

import logging

from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler

logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
logger = logging.getLogger(__name__)


def start(update, _):
    keyboard = [
        [
            InlineKeyboardButton("Option 1", callback_data='1'),
            InlineKeyboardButton("Option 2", callback_data='2'),
        ],
        [InlineKeyboardButton("Option 3", callback_data='3')],
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    # для версии 13.x
    update.message.reply_text('Пожалуйста, выберите:', reply_markup=reply_markup)
    # для версии 20.x необходимо использовать оператор await
    # await update.message.reply_text('Пожалуйста, выберите:', reply_markup=reply_markup)


def button(update, _):
    query = update.callback_query
    variant = query.data

    # `CallbackQueries` требует ответа, даже если 
    # уведомление для пользователя не требуется, в противном
    #  случае у некоторых клиентов могут возникнуть проблемы. 
    # смотри https://core.telegram.org/bots/api#callbackquery.
    query.answer()
    # для версии 20.x необходимо использовать оператор await
    # await query.answer()

    # редактируем сообщение, тем самым кнопки 
    # в чате заменятся на этот ответ.
    query.edit_message_text(text=f"Выбранный вариант: {variant}")
    # для версии 20.x необходимо использовать оператор await
    # await query.edit_message_text(text=f"Выбранный вариант: {variant}")

def help_command(update, _):
    update.message.reply_text("Используйте `/start` для тестирования.")
    # для версии 20.x необходимо использовать оператор await
    # await update.message.reply_text("Используйте `/start` для тестирования.")


if __name__ == '__main__':
    # в версии 13.x создаются 2 объекта:
    # `updater` и диспетчер `app`
    updater = Updater("TOKEN")
    app = updater.dispatcher
    # для версии 20.x необходимо создать только 1 объект 
    # приложение через `Application.builder()`
    # app = Application.builder().token("TOKEN").build()

    app.add_handler(CommandHandler('start', start))
    app.add_handler(CallbackQueryHandler(button))
    app.add_handler(CommandHandler('help', help_command))

    # Запуск бота в версии 13.x происходит 
    # через объект `updater`
    updater.start_polling()
    updater.idle()

    # Запуск бота в версии 20.x 
    # app.run_polling()

Пример встроенной клавиатуры с 2-мя состояниями для версии 13.x.

Данный пример снабжен комментариями, так что понять как и что работает не составит труда. Он так же демонстрирует использование обработчиков CallbackQueryHandler и ConversationHandler.

import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import (
    Updater,
    CommandHandler,
    CallbackQueryHandler,
    ConversationHandler,
)

# Ведение журнала логов
logging.basicConfig(
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)

logger = logging.getLogger(__name__)

# Этапы/состояния разговора
FIRST, SECOND = range(2)
# Данные обратного вызова
ONE, TWO, THREE, FOUR = range(4)


def start(update, _):
    """Вызывается по команде `/start`."""
    # Получаем пользователя, который запустил команду `/start`
    user = update.message.from_user
    logger.info("Пользователь %s начал разговор", user.first_name)
    # Создаем `InlineKeyboard`, где каждая кнопка имеет 
    # отображаемый текст и строку `callback_data`
    # Клавиатура - это список строк кнопок, где каждая строка, 
    # в свою очередь, является списком `[[...]]`
    keyboard = [
        [
            InlineKeyboardButton("1", callback_data=str(ONE)),
            InlineKeyboardButton("2", callback_data=str(TWO)),
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    # Отправляем сообщение с текстом и добавленной клавиатурой `reply_markup`
    update.message.reply_text(
        text="Запустите обработчик, выберите маршрут", reply_markup=reply_markup
    )
    # Сообщаем `ConversationHandler`, что сейчас состояние `FIRST`
    return FIRST


def start_over(update, _):
    """Тот же текст и клавиатура, что и при `/start`, но не как новое сообщение"""
    # Получаем `CallbackQuery` из обновления `update`
    query = update.callback_query
    # На запросы обратного вызова необходимо ответить, 
    # даже если уведомление для пользователя не требуется.
    # В противном случае у некоторых клиентов могут возникнуть проблемы.
    query.answer()
    keyboard = [
        [
            InlineKeyboardButton("1", callback_data=str(ONE)),
            InlineKeyboardButton("2", callback_data=str(TWO)),
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
   # Отредактируем сообщение, вызвавшее обратный вызов.
   # Это создает ощущение интерактивного меню.
    query.edit_message_text(
        text="Выберите маршрут", reply_markup=reply_markup
    )
    # Сообщаем `ConversationHandler`, что сейчас находимся в состоянии `FIRST`
    return FIRST


def one(update, _):
    """Показ нового выбора кнопок"""
    query = update.callback_query
    query.answer()
    keyboard = [
        [
            InlineKeyboardButton("3", callback_data=str(THREE)),
            InlineKeyboardButton("4", callback_data=str(FOUR)),
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    query.edit_message_text(
        text="Вызов `CallbackQueryHandler`, выберите маршрут", reply_markup=reply_markup
    )
    return FIRST


def two(update, _):
    """Показ нового выбора кнопок"""
    query = update.callback_query
    query.answer()
    keyboard = [
        [
            InlineKeyboardButton("1", callback_data=str(ONE)),
            InlineKeyboardButton("3", callback_data=str(THREE)),
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    query.edit_message_text(
        text="Второй CallbackQueryHandler", reply_markup=reply_markup
    )
    return FIRST


def three(update, _):
    """Показ выбора кнопок"""
    query = update.callback_query
    query.answer()
    keyboard = [
        [
            InlineKeyboardButton("Да, сделаем это снова!", callback_data=str(ONE)),
            InlineKeyboardButton("Нет, с меня хватит ...", callback_data=str(TWO)),
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    query.edit_message_text(
        text="Третий CallbackQueryHandler. Начать сначала?", reply_markup=reply_markup
    )
    # Переход в состояние разговора `SECOND`
    return SECOND


def four(update, _):
    """Показ выбора кнопок"""
    query = update.callback_query
    query.answer()
    keyboard = [
        [
            InlineKeyboardButton("2", callback_data=str(TWO)),
            InlineKeyboardButton("4", callback_data=str(FOUR)),
        ]
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    query.edit_message_text(
        text="Четвертый CallbackQueryHandler, выберите маршрут", reply_markup=reply_markup
    )
    return FIRST


def end(update, _):
    """Возвращает `ConversationHandler.END`, который говорит 
    `ConversationHandler` что разговор окончен"""
    query = update.callback_query
    query.answer()
    query.edit_message_text(text="See you next time!")
    return ConversationHandler.END


if __name__ == '__main__':
    updater = Updater("TOKEN")
    dispatcher = updater.dispatcher

    # Настройка обработчика разговоров с состояниями `FIRST` и `SECOND`
    # Используем параметр `pattern` для передачи `CallbackQueries` с
    # определенным шаблоном данных соответствующим обработчикам
    # ^ - означает "начало строки"
    # $ - означает "конец строки"
    # Таким образом, паттерн `^ABC$` будет ловить только 'ABC'
    conv_handler = ConversationHandler(
        entry_points=[CommandHandler('start', start)],
        states={ # словарь состояний разговора, возвращаемых callback функциями
            FIRST: [
                CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
                CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
                CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
                CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$'),
            ],
            SECOND: [
                CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
                CallbackQueryHandler(end, pattern='^' + str(TWO) + '$'),
            ],
        },
        fallbacks=[CommandHandler('start', start)],
    )

    # Добавляем `ConversationHandler` в диспетчер, который
    # будет использоваться для обработки обновлений
    dispatcher.add_handler(conv_handler)

    updater.start_polling()
    updater.idle()

Пример встроенной клавиатуры с 2-мя состояниями для версии 20.x.

from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, Update
from telegram.ext import (
    Application,
    CommandHandler,
    ContextTypes,
    ConversationHandler,
    MessageHandler,
    filters,
)

logging.basicConfig(
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=logging.INFO
)
logger = logging.getLogger(__name__)

CHOOSING, TYPING_REPLY, TYPING_CHOICE = range(3)

reply_keyboard = [
    ["Age", "Favourite colour"],
    ["Number of siblings", "Something else..."],
    ["Done"],
]
markup = ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)

def facts_to_str(user_data: Dict[str, str]) -> str:
    """Вспомогательная функция для форматирования 
    собранной информации о пользователе."""
    facts = [f"{key} - {value}" for key, value in user_data.items()]
    return "\n".join(facts).join(["\n", "\n"])

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Начvало разговора, просьба ввести данные."""
    await update.message.reply_text(
        "Hi! My name is Doctor Botter. I will hold a more complex conversation with you. "
        "Why don't you tell me something about yourself?",
        reply_markup=markup,
    )
    return CHOOSING

async def regular_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Запрос информации о выбранном предопределенном выборе."""
    text = update.message.text
    context.user_data["choice"] = text
    await update.message.reply_text(f"Your {text.lower()}? Yes, I would love to hear about that!")
    return TYPING_REPLY

async def custom_choice(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Запрос описания пользовательской категории."""
    await update.message.reply_text(
        'Alright, please send me the category first, for example "Most impressive skill"'
    )
    return TYPING_CHOICE

async def received_information(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Store info provided by user and ask for the next category."""
    user_data = context.user_data
    text = update.message.text
    category = user_data["choice"]
    user_data[category] = text
    del user_data["choice"]

    await update.message.reply_text(
        "Neat! Just so you know, this is what you already told me:"
        f"{facts_to_str(user_data)}You can tell me more, or change your opinion"
        " on something.",
        reply_markup=markup,
    )
    return CHOOSING

async def done(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
    """Вывод собранной информации и завершение разговора."""
    user_data = context.user_data
    if "choice" in user_data:
        del user_data["choice"]

    await update.message.reply_text(
        f"I learned these facts about you: {facts_to_str(user_data)}Until next time!",
        reply_markup=ReplyKeyboardRemove(),
    )
    user_data.clear()
    return ConversationHandler.END


if __name__ == "__main__":
    application = Application.builder().token("TOKEN").build()

    conv_handler = ConversationHandler(
        entry_points=[CommandHandler("start", start)],
        states={
            CHOOSING: [
                MessageHandler(
                    filters.Regex("^(Age|Favourite colour|Number of siblings)$"), regular_choice
                ),
                MessageHandler(filters.Regex("^Something else...$"), custom_choice),
            ],
            TYPING_CHOICE: [
                MessageHandler(
                    filters.TEXT & ~(filters.COMMAND | filters.Regex("^Done$")), regular_choice
                )
            ],
            TYPING_REPLY: [
                MessageHandler(
                    filters.TEXT & ~(filters.COMMAND | filters.Regex("^Done$")),
                    received_information,
                )
            ],
        },
        fallbacks=[MessageHandler(filters.Regex("^Done$"), done)],
    )

    application.add_handler(conv_handler)
    # Запуск бота.
    application.run_polling()

Как работает обработчик разговора ConversationHandler().

Основная магия происходит в обработчике разговора ConversationHandler(). Обработчик ConversationHandler() имеет три основные точки, которые необходимо определить для ведения беседы:

  • entry_points - точка входа в разговор, представляет собой список обработчиков, которые запускают разговор. Разговор можно запустить по команде, отправленной пользователем (в данном случае /start) и/или по каким то фразам, которые можно поймать при помощи обработчика MessageHandler() и фильтра Filters.regex (например: Filters.regex('(поговорим|скучно)'), callback_func)],
  • states - состояния разговора. Представляет собой словарь, в котором ключ, это этап разговора, который явно возвращает функция обратного вызова, при этом высылает или отвечает на сообщение или передает кнопки для выбора и т.д. Так вот, реакция/ответ пользователя на это сообщение/нажатие кнопки будет обрабатываться обработчиками, находящихся в списке значений этого ключа - этапа/состояния разговора.
  • fallbacks - точка выхода из разговора. Разговор заканчивается, если функция обработчик сообщения явно возвращает return ConversationHandler.END

Переключение между этапами разговора происходит при помощи функций обратного вызова, которые при обработке/анализе ответа пользователя будут возвращать нужный этап/состояние разговора.

conv_handler = ConversationHandler(
        # точка входа в разговор
        entry_points=[CommandHandler('start', start)],
        # словарь состояний разговора, возвращаемых callback функциями
        states={ 
            # Этап `FIRST` - т.е. функция обработчик какого то сообщения явно 
            # вернула константу FIRST (return `FIRST`), а так же послала/ответила 
            # на сообщение. Ответ пользователя на это сообщение будет 
            # обрабатываться обработчиками определенными в этом списке 
            FIRST: [
                CallbackQueryHandler(one, pattern='^' + str(ONE) + '$'),
                CallbackQueryHandler(two, pattern='^' + str(TWO) + '$'),
                CallbackQueryHandler(three, pattern='^' + str(THREE) + '$'),
                CallbackQueryHandler(four, pattern='^' + str(FOUR) + '$'),
            ],
            # Этап `SECOND` - происходит то же самое, что и в описании этапа `FIRST`
            SECOND: [
                CallbackQueryHandler(start_over, pattern='^' + str(ONE) + '$'),
                CallbackQueryHandler(end, pattern='^' + str(TWO) + '$'),
            ],
        },
        # точка выхода из разговора 
        fallbacks=[CommandHandler('start', start)],
    )