Внимание! Пакеты
python-telegram-bot
версии 13.x будут придерживаться многопоточной парадигмы программирования (*на данный момент актуальна версия 13.15). Пакеты версий 20.x и новее предоставляют чистый асинхронный Python интерфейс для Telegram Bot API. Дополнительно смотрите основные изменения в пакетеpython-telegram-bot
версии 20.x.
Всякий раз, когда бот отправляет сообщение, он может передать специальную клавиатуру с предопределенными параметрами ответа. Приложения Telegram, которые получают сообщение, будут отображать эту клавиатуру для пользователя. Нажатие любой из кнопок немедленно отправит соответствующую команду. Таким образом можно значительно упростить взаимодействие пользователя с ботом.
KeyboardButton
и InlineKeyboardButton
;CallbackQueryHandler
;ConversationHandler()
.Бывают случаи, когда нужно что-либо сделать, не отправляя никаких сообщений в чат. Например, когда пользователь меняет настройки или просматривает результаты поиска. В таких случаях можно использовать встроенные 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
) - произвольные ключевые аргументы.Для создания макета кнопок со столбцами 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()
Данный пример снабжен комментариями, так что понять как и что работает не составит труда. Он так же демонстрирует использование обработчиков 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()
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)], )