Кнопки¶
В этой главе мы познакомимся с такой замечательной фичей Telegram-ботов как кнопки. Прежде всего, чтобы избежать путаницы, определимся с названиями. То, что цепляется к низу экрана вашего устройства, будем называть обычными кнопками, а то, что цепляется непосредственно к сообщениям, назовём инлайн-кнопками. Ещё раз картинкой:

Обычные кнопки¶
Кнопки как шаблоны¶
Этот вид кнопок появился вместе с Bot API в далёком 2015 году и представляет собой не что иное, как шаблоны сообщений (за исключением нескольких особых случаев, но о них позже). Принцип простой: что написано на кнопке, то и будет отправлено в текущий чат. Соответственно, чтобы обработать нажатие такой кнопки, бот должен распознавать входящие текстовые сообщения.
Напишем хэндлер, который будет при нажатии на команду /start отправлять сообщение с двумя кнопками:
@dp.message(Command("start")) async def cmd_start(message: types.Message): kb = [ [types.KeyboardButton(text="С пюрешкой")], [types.KeyboardButton(text="Без пюрешки")] ] keyboard = types.ReplyKeyboardMarkup(keyboard=kb) await message.answer("Как подавать котлеты?", reply_markup=keyboard)
Несмотря на то, что Telegram Bot API допускает указывать просто строки вместо объектов KeyboardButton , при попытке использовать строку aiogram 3.x выкинет ошибку валидации и это не баг, а фича.
Живите теперь с этим ♂️
Что ж, запустим бота и обалдеем от громадных кнопок:

Как-то некрасиво. Во-первых, хочется сделать кнопки поменьше, а во-вторых, расположить их горизонтально.
Почему вообще они такие большие? Дело в том, что по умолчанию «кнопочная» клавиатура должна занимать на смартфонах столько же места, сколько и обычная буквенная. Для уменьшения кнопок к объекту клавиатуры надо указать дополнительный параметр resize_keyboard=True .
Но как заменить вертикальные кнопки на горизонтальные? С точки зрения Bot API, клавиатура — это массив массивов кнопок, а если говорить проще, массив рядов. Перепишем наш код, чтобы было красиво, а для пущей важности добавим параметр input_field_placeholder , который заменит текст в пустой строке ввода, когда активна обычная клавиатура:
@dp.message(Command("start")) async def cmd_start(message: types.Message): kb = [ [ types.KeyboardButton(text="С пюрешкой"), types.KeyboardButton(text="Без пюрешки") ], ] keyboard = types.ReplyKeyboardMarkup( keyboard=kb, resize_keyboard=True, input_field_placeholder="Выберите способ подачи" ) await message.answer("Как подавать котлеты?", reply_markup=keyboard)
Смотрим — действительно красиво:

Осталось научить бота реагировать на нажатие таких кнопок. Как уже было сказано выше, необходимо делать проверку на полное совпадение текста. Сделаем это при помощи магического фильтра F, подробнее о котором поговорим в другой главе:
# новый импорт! from aiogram import F @dp.message(F.text.lower() == "с пюрешкой") async def with_puree(message: types.Message): await message.reply("Отличный выбор!") @dp.message(F.text.lower() == "без пюрешки") async def without_puree(message: types.Message): await message.reply("Так невкусно!")

Чтобы удалить кнопки, необходимо отправить новое сообщение со специальной «удаляющей» клавиатурой типа ReplyKeyboardRemove . Например: await message.reply(«Отличный выбор!», reply_markup=types.ReplyKeyboardRemove())
Keyboard Builder¶
Для более динамической генерации кнопок можно воспользоваться сборщиком клавиатур. Нам пригодятся следующие методы:
- add() — добавляет кнопку в память сборщика;
- adjust(int1, int2, int3. ) — делает строки по int1, int2, int3. кнопок;
- as_markup() — возвращает готовый объект клавиатуры;
- button() — добавляет кнопку с заданными параметрами, тип кнопки (Reply или Inline) определяется автоматически.
Создадим пронумерованную клавиатуру размером 4×4:
# новый импорт! from aiogram.utils.keyboard import ReplyKeyboardBuilder @dp.message(Command("reply_builder")) async def reply_builder(message: types.Message): builder = ReplyKeyboardBuilder() for i in range(1, 17): builder.add(types.KeyboardButton(text=str(i))) builder.adjust(4) await message.answer( "Выберите число:", reply_markup=builder.as_markup(resize_keyboard=True), )

У объекта обычной клавиатуры есть ещё две полезных опции: one_time_keyboard для автоматического скрытия кнопок после нажатия и selective для показа клавиатуры лишь некоторым участникам группы. Их использование остаётся для самостоятельного изучения.
Специальные обычные кнопки¶
На момент написания этой главы в Telegram существует шесть специальных видов обычных кнопок, не являющихся обычными шаблонами сообщений. Они предназначены для:
- отправки текущей геолокации;
- отправки своего контакта с номером телефона;
- создания опроса/викторины;
- выбора и отправки боту данных пользователя с нужными критериями;
- выбора и отправки боту данных (супер)группы или канала с нужными критериями;
- запуска веб-приложения (WebApp).
Поговорим про них подробнее.
Отправка текущей геолокации. Здесь всё просто: где пользователь находится, те координаты и отправляет. Это будет статическое гео, а не Live Location, который обновляется автоматически. Разумеется, хитрые юзеры могут подменить своё местонахождение, иногда даже на уровне всей системы (Android).
Отправка своего контакта с номером телефона. При нажатии на кнопку (с предварительным подтверждением) пользователь отправляет свой контакт с номером телефона боту. Те же хитрые юзеры могут проигнорировать кнопку и отправить любой контакт, но в этом случае на них можно найти управу: достаточно проверить в хэндлере или в фильтре равенство message.contact.user_id == message.from_user.id .
Создание опроса/викторины. По нажатию на кнопку пользователю предлагается создать опрос или викторину, которые потом отправятся в текущий чат. Необходимо передать объект KeyboardButtonPollType, необязательный аргумент type служит для уточнения типа опроса (опрос или викторина).
Выбор и отправка боту данных пользователя с нужными критериями. Показывает окно выбора пользователя из списка чатов юзера, нажавшего на кнопку. Необходимо передать объект KeyboardButtonRequestUser, в котором надо указать сгенерированный любым способом айди запроса и критерии, например, «бот», «есть подписка Telegram Premium» и т.д. После выбора юзера бот получит сервисное сообщение с типом UserShared.
Выбор и отправка боту чата с нужными критериями. Показывает окно выбора пользователя из списка чатов юзера, нажавшего на кнопку. Необходимо передать объект KeyboardButtonRequestChat, в котором надо указать сгенерированный любым способом айди запроса и критерии, например, «группа или канал», «юзер — создатель чата» и т.д. После выбора юзера бот получит сервисное сообщение с типом ChatShared.
Запуск веб-приложения (WebApp). При нажатии на кнопку открывает WebApp. Необходимо передать объект WebAppInfo. В этой книге веб-аппы пока рассматриваться не будут.
Впрочем, проще один раз увидеть код:
@dp.message(Command("special_buttons")) async def cmd_special_buttons(message: types.Message): builder = ReplyKeyboardBuilder() # метод row позволяет явным образом сформировать ряд # из одной или нескольких кнопок. Например, первый ряд # будет состоять из двух кнопок. builder.row( types.KeyboardButton(text="Запросить геолокацию", request_location=True), types.KeyboardButton(text="Запросить контакт", request_contact=True) ) # . второй из одной . builder.row(types.KeyboardButton( text="Создать викторину", request_poll=types.KeyboardButtonPollType(type="quiz")) ) # . а третий снова из двух builder.row( types.KeyboardButton( text="Выбрать премиум пользователя", request_user=types.KeyboardButtonRequestUser( request_id=1, user_is_premium=True ) ), types.KeyboardButton( text="Выбрать супергруппу с форумами", request_chat=types.KeyboardButtonRequestChat( request_id=2, chat_is_channel=False, chat_is_forum=True ) ) ) # WebApp-ов пока нет, сорри :( await message.answer( "Выберите действие:", reply_markup=builder.as_markup(resize_keyboard=True), )

Напоследок, две заготовки хэндлеров на приём нажатий от нижних двух кнопок:
# новый импорт from aiogram import F @dp.message(F.user_shared) async def on_user_shared(message: types.Message): print( f"Request message.user_shared.request_id>. " f"User ID: message.user_shared.user_id>" ) @dp.message(F.chat_shared) async def on_user_shared(message: types.Message): print( f"Request message.chat_shared.request_id>. " f"User ID: message.chat_shared.chat_id>" )
Инлайн-кнопки¶
URL-кнопки¶
В отличие от обычных кнопок, инлайновые цепляются не к низу экрана, а к сообщению, с которым были отправлены. В этой главе мы рассмотрим два типа таких кнопок: URL и Callback. Ещё один — Switch — будет рассмотрен в главе про инлайн-режим.
Login- и Pay-кнопки в книге рассматриваться не будут вообще. Если у кого-то есть желание помочь хотя бы с рабочим кодом для авторизации или оплаты, пожалуйста, создайте Pull Request на GitHub. Спасибо!
Самые простые инлайн-кнопки относятся к типу URL, т.е. «ссылка». Поддерживаются только протоколы HTTP(S) и tg://
# новый импорт from aiogram.utils.keyboard import InlineKeyboardBuilder @dp.message(Command("inline_url")) async def cmd_inline_url(message: types.Message, bot: Bot): builder = InlineKeyboardBuilder() builder.row(types.InlineKeyboardButton( text="GitHub", url="https://github.com") ) builder.row(types.InlineKeyboardButton( text="Оф. канал Telegram", url="tg://resolve?domain=telegram") ) # Чтобы иметь возможность показать ID-кнопку, # У юзера должен быть False флаг has_private_forwards user_id = 1234567890 chat_info = await bot.get_chat(user_id) if not chat_info.has_private_forwards: builder.row(types.InlineKeyboardButton( text="Какой-то пользователь", url=f"tg://user?id=user_id>") ) await message.answer( 'Выберите ссылку', reply_markup=builder.as_markup(), )
Отдельно остановимся на среднем блоке кода. Дело в том, что в марте 2019 года разработчики Telegram добавили возможность отключать переход к профилю пользователя у пересланного сообщения. При попытке создать URL-кнопку с ID юзера, у которого отключен переход по форварду, бот получит ошибку Bad Request: BUTTON_USER_PRIVACY_RESTRICTED . Соответственно, прежде чем показывать такую кнопку, необходимо выяснить состояние упомянутой настройки. Для этого можно вызвать метод getChat и в ответе проверить состояние поля has_private_forwards . Если оно равно True , значит, попытка добавить URL-ID кнопку приведёт к ошибке.
Колбэки¶
С URL-кнопками больше обсуждать, по сути, нечего, поэтому перейдём к гвоздю сегодняшней программы — Callback-кнопкам. Это очень мощная штука, которую вы можете встретить практически везде. Кнопки-реакции у постов (лайки), меню у @BotFather и т.д. Суть в чём: у колбэк-кнопок есть специальное значение (data), по которому ваше приложение опознаёт, что нажато и что надо сделать. И выбор правильного data очень важен! Стоит также отметить, что, в отличие от обычных кнопок, нажатие на колбэк-кнопку позволяет сделать практически что угодно, от заказа пиццы до запуска вычислений на кластере суперкомпьютеров.
Напишем хэндлер, который по команде /random будет отправлять сообщение с колбэк-кнопкой:
@dp.message(Command("random")) async def cmd_random(message: types.Message): builder = InlineKeyboardBuilder() builder.add(types.InlineKeyboardButton( text="Нажми меня", callback_data="random_value") ) await message.answer( "Нажмите на кнопку, чтобы бот отправил число от 1 до 10", reply_markup=builder.as_markup() )
Но как же обработать нажатие? Если раньше мы использовали хэндлер на message для обработки входящих сообщений, то теперь будем использовать хэндлер на callback_query для обработки колбэков. Ориентироваться будем на «значение» кнопки, т.е. на её data:
@dp.callback_query(F.data == "random_value") async def send_random_value(callback: types.CallbackQuery): await callback.message.answer(str(randint(1, 10)))

Ой, а что это за часики? Оказывается, сервер Telegram ждёт от нас подтверждения о доставке колбэка, иначе в течение 30 секунд будет показывать специальную иконку. Чтобы скрыть часики, нужно вызвать метод answer() у колбэка (или использовать метод API answer_callback_query() ). В общем случае, в метод answer() можно ничего не передавать, но можно вызвать специальное окошко (всплывающее сверху или поверх экрана):
@dp.callback_query(F.data == "random_value") async def send_random_value(callback: types.CallbackQuery): await callback.message.answer(str(randint(1, 10))) await callback.answer( text="Спасибо, что воспользовались ботом!", show_alert=True ) # или просто await callback.answer()

У читателя может возникнуть вопрос: в какой момент обработки отвечать на колбэк методом answer() ? В общем случае, главное — просто не забыть сообщить Telegram о получении колбэк-запроса, но я рекомендую ставить вызов answer() в самом конце, и вот почему: если вдруг в процессе обработки колбэка случится какая-то ошибка и бот нарвётся на необработанное исключение, пользователь увидит неубирающиеся полминуты часики и поймёт, что что-то не так. В противном случае часики исчезнут, а пользователь останется в неведении, выполнился его запрос успешно или нет.
В функции send_random_value мы вызывали метод answer() не у message , а у callback.message . Это связано с тем, что колбэк-хэндлеры работают не с сообщениями (тип Message), а с колбэками (тип CallbackQuery), у которого другие поля, и само сообщение — всего лишь его часть. Учтите также, что message — это сообщение, к которому была прицеплена кнопка (т.е. отправитель такого сообщения — сам бот). Если хотите узнать, кто нажал на кнопку, смотрите поле from (в вашем коде это будет callback.from_user , т.к. слово from зарезервировано в Python)
Про объект message в колбэке
Если сообщение отправлено из инлайн-режима, то поле message у колбэка будет пустым. У вас не будет возможности получить содержимое такого сообщения, если только заранее где-то его не сохранить.
Перейдём к примеру посложнее. Пусть пользователю предлагается сообщение с числом 0, а внизу три кнопки: +1, -1 и Подтвердить. Первыми двумя он может редактировать число, а последняя удаляет всю клавиатуру, фиксируя изменения. Хранить значения будем в памяти в словаре (про конечные автоматы поговорим как-нибудь в другой раз).
# Здесь хранятся пользовательские данные. # Т.к. это словарь в памяти, то при перезапуске он очистится user_data = <> def get_keyboard(): buttons = [ [ types.InlineKeyboardButton(text="-1", callback_data="num_decr"), types.InlineKeyboardButton(text="+1", callback_data="num_incr") ], [types.InlineKeyboardButton(text="Подтвердить", callback_data="num_finish")] ] keyboard = types.InlineKeyboardMarkup(inline_keyboard=buttons) return keyboard async def update_num_text(message: types.Message, new_value: int): await message.edit_text( f"Укажите число: new_value>", reply_markup=get_keyboard() ) @dp.message(Command("numbers")) async def cmd_numbers(message: types.Message): user_data[message.from_user.id] = 0 await message.answer("Укажите число: 0", reply_markup=get_keyboard()) @dp.callback_query(F.data.startswith("num_")) async def callbacks_num(callback: types.CallbackQuery): user_value = user_data.get(callback.from_user.id, 0) action = callback.data.split("_")[1] if action == "incr": user_data[callback.from_user.id] = user_value+1 await update_num_text(callback.message, user_value+1) elif action == "decr": user_data[callback.from_user.id] = user_value-1 await update_num_text(callback.message, user_value-1) elif action == "finish": await callback.message.edit_text(f"Итого: user_value>") await callback.answer()
И, казалось бы, всё работает:

Но теперь представим, что ушлый пользователь сделал следующее: вызвал команду /numbers (значение 0), увеличил значение до 1, снова вызвал /numbers (значение сбросилось до 0) и отредактировал и нажал кнопку «+1» на первом сообщении. Что произойдёт? Бот по-честному отправит запрос на редактирование текста со значением 1, но т.к. на том сообщении уже стоит цифра 1, то Bot API вернёт ошибку, что старый и новый тексты совпадают, а бот словит исключение: Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message

С этой ошибкой вы, скорее всего, будете поначалу часто сталкиваться, пытаясь редактировать сообщения. Вообще говоря, подобная ошибка часто говорит о проблемах с логикой генерации/обновления данных в сообщении, но иногда, как в примере выше, может быть ожидаемым поведением.
В данном случае проигнорируем ошибку целиком, т.к. нам важен лишь итоговый результат, который точно будет правильным. Ошибка MessageNotModified относится к категории Bad Request, поэтому у нас есть выбор: проигнорировать весь подобный класс ошибок, либо отловить весь класс BadRequest и попытаться по тексту ошибки опознать конкретную причину. Чтобы не слишком усложнять пример, обойдёмся первым способом и немного обновим функцию update_num_text() :
# Новые импорты! from contextlib import suppress from aiogram.exceptions import TelegramBadRequest async def update_num_text(message: types.Message, new_value: int): with suppress(TelegramBadRequest): await message.edit_text( f"Укажите число: new_value>", reply_markup=get_keyboard() )
Если теперь вы попробуете повторить пример выше, то указанное исключение в этом блоке кода бот просто-напросто проигнорирует.
Фабрика колбэков¶
Когда вы оперируете какими-то простыми колбэками с общим префиксом, типа order_1 , order_2 . вам может показаться, что довольно легко вызывать split() и делить строку по какому-то разделителю. А теперь представьте, что вам нужно хранить не одно значение, а три: order_1_1994_2731519 . Что здесь артикул, цена, количество? А может быть, тут вообще год выпуска? Да и разбиение строки начинает выглядеть страшно: .split(«_»)[2] . А почему не 1 или 3?
В какой-то момент возникает необходимость структурировать содержимое таких callback data, и в aiogram есть решение! Вы создаёте объекты типа CallbackData , указываете префикс, описываете структуру, а дальше фреймворк самостоятельно собирает строку с данными колбэка и, что важнее, корректно разбирает входящее значение. Снова разберёмся на конкретном примере; создадим класс NumbersCallbackFactory с префиксом fabnum и двумя полями action и value . Поле action определяет, что делать, менять значение (change) или зафиксировать (finish), а поле value показывает, на сколько изменять значение. По умолчанию оно будет None, т.к. для действия «finish» дельта изменения не требуется. Код:
# новые импорты! from typing import Optional from aiogram.filters.callback_data import CallbackData class NumbersCallbackFactory(CallbackData, prefix="fabnum"): action: str value: Optional[int] = None
Наш класс обязательно должен наследоваться от CallbackData и принимать значение префикса. Префикс — это общая подстрока в начале, по которой фреймворк будет определять, какая структура лежит в колбэке.
Теперь напишем функцию генерации клавиатуры. Здесь нам пригодится метод button() , который автоматически будет создавать кнопку с нужным типом, а от нас требуется только передать аргументы. В качестве аргумента callback_data вместо строки будем указывать экземпляр нашего класса NumbersCallbackFactory :
def get_keyboard_fab(): builder = InlineKeyboardBuilder() builder.button( text="-2", callback_data=NumbersCallbackFactory(action="change", value=-2) ) builder.button( text="-1", callback_data=NumbersCallbackFactory(action="change", value=-1) ) builder.button( text="+1", callback_data=NumbersCallbackFactory(action="change", value=1) ) builder.button( text="+2", callback_data=NumbersCallbackFactory(action="change", value=2) ) builder.button( text="Подтвердить", callback_data=NumbersCallbackFactory(action="finish") ) # Выравниваем кнопки по 4 в ряд, чтобы получилось 4 + 1 builder.adjust(4) return builder.as_markup()
Методы отправки сообщения и его редактирования оставляем теми же (в названиях и командах добавим суффикс _fab ):
async def update_num_text_fab(message: types.Message, new_value: int): with suppress(TelegramBadRequest): await message.edit_text( f"Укажите число: new_value>", reply_markup=get_keyboard_fab() ) @dp.message(Command("numbers_fab")) async def cmd_numbers_fab(message: types.Message): user_data[message.from_user.id] = 0 await message.answer("Укажите число: 0", reply_markup=get_keyboard_fab())
Наконец, переходим к главному — обработке колбэков. Для этого в декоратор надо передать класс, колбэки с которым мы ловим, с вызванным методом filter() . Также появляется дополнительный аргумент с названием callback_data (имя должно быть именно таким!), и имеющим тот же тип, что и фильтруемый класс:
@dp.callback_query(NumbersCallbackFactory.filter()) async def callbacks_num_change_fab( callback: types.CallbackQuery, callback_data: NumbersCallbackFactory ): # Текущее значение user_value = user_data.get(callback.from_user.id, 0) # Если число нужно изменить if callback_data.action == "change": user_data[callback.from_user.id] = user_value + callback_data.value await update_num_text_fab(callback.message, user_value + callback_data.value) # Если число нужно зафиксировать else: await callback.message.edit_text(f"Итого: user_value>") await callback.answer()
Ещё немного конкретизируем наши хэндлеры и сделаем отдельный обработчик для числовых кнопок и для кнопки «Подтвердить». Фильтровать будем по значению action и в этом нам помогут «магические фильтры» aiogram 3.x. Серьёзно, они так и называются: Magic Filter. Подробнее сие чародейство рассмотрим в другой главе, а сейчас просто воспользуемся «магией» и примем это на веру:
# новый импорт! from magic_filter import F # Нажатие на одну из кнопок: -2, -1, +1, +2 @dp.callback_query(NumbersCallbackFactory.filter(F.action == "change")) async def callbacks_num_change_fab( callback: types.CallbackQuery, callback_data: NumbersCallbackFactory ): # Текущее значение user_value = user_data.get(callback.from_user.id, 0) user_data[callback.from_user.id] = user_value + callback_data.value await update_num_text_fab(callback.message, user_value + callback_data.value) await callback.answer() # Нажатие на кнопку "подтвердить" @dp.callback_query(NumbersCallbackFactory.filter(F.action == "finish")) async def callbacks_num_finish_fab(callback: types.CallbackQuery): # Текущее значение user_value = user_data.get(callback.from_user.id, 0) await callback.message.edit_text(f"Итого: user_value>") await callback.answer()

На первый взгляд то, что мы сделали, может показаться сложным, но в действительности фабрика колбэков позволяет создавать продвинутые колбэк-кнопки и удобно дробить код на логические сущности. Увидеть применение фабрики на практике вы можете в боте для игры в «Сапёра», написанным вашим любимым автором 🙂
Автоответ на колбэки¶
Если у вас очень много колбэк-хэндлеров, на которые нужно либо просто отвечать, либо отвечать однотипно, можно немного упростить себе жизнь, воспользовавшись специальной мидлварью. В целом про такое мы поговорим отдельно, а сейчас просто познакомимся.
Итак, самый простой вариант — это добавить вот такую строчку после создания диспетчера:
# не забываем про новый импорт from aiogram.utils.callback_answer import CallbackAnswerMiddleware dp = Dispatcher() dp.callback_query.middleware(CallbackAnswerMiddleware())
В этом случае после выполнения хэндлера aiogram будет автоматически отвечать на колбэк. Можно переопределить стандартные настройки и указать свои, например:
dp.callback_query.middleware( CallbackAnswerMiddleware( pre=True, text="Готово!", show_alert=True ) )
Увы, ситуации, когда на все колбэк-хэндлеры одинаковый ответ, довольно редки. К счастью, переопределить поведение мидлвари в конкретном обработчике довольно просто: достаточно пробросить аргумент callback_answer и выставить ему новые значения:
# новый импорт! from aiogram.utils.callback_answer import CallbackAnswer @dp.callback_query() async def my_handler(callback: CallbackQuery, callback_answer: CallbackAnswer): . # тут какой-то код if everything is ok>: callback_answer.text = "Отлично!" else: callback_answer.text = "Что-то пошло не так. Попробуйте позже" callback_answer.cache_time = 10 . # тут какой-то код
Важно: этот способ не будет работать, если у мидлвари выставлен флаг pre=True . В этом случае надо полностью переопределять набор параметров мидлвари через флаги, с которыми мы подробнее познакомимся позже:
from aiogram import flags from aiogram.utils.callback_answer import CallbackAnswer @dp.callback_query() @flags.callback_answer(pre=False) # переопределяем флаг pre async def my_handler(callback: CallbackQuery, callback_answer: CallbackAnswer): . # тут какой-то код if everything is ok>: callback_answer.text = "Теперь этот текст будет видно!" . # тут какой-то код
На этом мы пока завершим знакомство с кнопками.
Меню из кнопок, модуль python-telegram-bot
Внимание! Пакеты python-telegram-bot версии 13.x будут придерживаться многопоточной парадигмы программирования (*на данный момент актуальна версия 13.15). Пакеты версий 20.x и новее предоставляют чистый асинхронный Python интерфейс для Telegram Bot API. Дополнительно смотрите основные изменения в пакете python-telegram-bot версии 20.x.
Всякий раз, когда бот отправляет сообщение, он может передать специальную клавиатуру с предопределенными параметрами ответа. Приложения Telegram, которые получают сообщение, будут отображать эту клавиатуру для пользователя. Нажатие любой из кнопок немедленно отправит соответствующую команду. Таким образом можно значительно упростить взаимодействие пользователя с ботом.
Содержание.
- Встроенные клавиатуры Telegramm в сообщения бота;
- Классы KeyboardButton и InlineKeyboardButton ;
- Алгоритм построения и отправки кнопок в Telegram чат;
- Обработчик сообщений CallbackQueryHandler ;
- Базовый пример, использующий встроенную клавиатуру;
- Пример встроенной клавиатуры с 2-мя состояниями для версии 13.x;
- Пример встроенной клавиатуры с 2-мя состояниями для версии 20.x;
- Как работает обработчике разговора ConversationHandler() .
Встроенные клавиатуры 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"Выбранный вариант: ") 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)], )
- КРАТКИЙ ОБЗОР МАТЕРИАЛА.
- Переход на асинхронный python-telegram-bot версии 20.x
- Чистый интерфейс Python для Telegram Bot API
- Команды и оповещения @BotFather в Telegram
- Обработка сообщений модулем python-telegram-bot
- Фильтры сообщений модуля python-telegram-bot
- Хранение временных данных модулем python-telegram-bot
- Настройки по умолчанию модуля python-telegram-bot
- Планировщик сообщений модуля python-telegram-bot
- Форматирование и отправка сообщений в python-telegram-bot
- Работа с файлами/media, модуль python-telegram-bot
- Меню из кнопок, модуль python-telegram-bot
- Объект CallbackContext модуля python-telegram-bot
- Подключения Telegram-бота через webhook
- Обработка исключений модуля python-telegram-bot
- Создание Inline-бота, модуль python-telegram-bot
- Работа с опросами в модуле python-telegram-bot
- Создание разговоров ConversationHandler в python-telegram-bot
- Перезапуск телеграмм-бота в случае ошибки
- Декоратор-обработчик сообщений в python-telegram-bot
- Авторизация на сайте через Telegram Passport
- Ведение публикаций в Telegram-канале с python-telegram-bot
- UTF коды emoji/эмодзи для отправки в Telegram из Python
Как удалить кнопку из клавиатуры telebot?

Нельзя удалить одну кнопку. Можно либо удалить всю клавиатуру целиком, либо отправить другую клавиатуру на замену. Соответственно, для удаления всего одной кнопки нужно помнить, какую клавиатуру отправлял в прошлый раз (хранить это явным образом или определять из контекста), чтобы отправить новую клавиатуру без этой кнопки.
Ответ написан более года назад
Комментировать
Нравится Комментировать
Ваш ответ на вопрос
Войдите, чтобы написать ответ

- Python
Опять коги! Выдаёт ошибку, как её убрать?
- 1 подписчик
- 4 минуты назад
- 8 просмотров

- Node.js
- +4 ещё
Как сконфигурировать бэк и фронт через api?
- 2 подписчика
- час назад
- 58 просмотров
Bot API v2: Кнопки и редактирование сообщений
В начале апреля 2016 года вышло первое по-настоящему крупное обновление API для ботов. Изменений довольно много, поэтому материал я разобью на несколько частей. Сегодня поговорим об inline-кнопках и редактировании сообщений, а затем обсудим новые инлайн-режимы вместе со специальными кнопками для отправки геолокации и номера телефона.
Новые возможности
- Каждая кнопка, будь то обычная или инлайн, это теперь самостоятельный объект KeyboardButton или InlineKeyboardButton , не забудьте обновить своих ботов!
- В Inline-режиме все текстовые поля теперь представлены отдельными объектами InputMessageContent , которые, в свою очередь могут быть аж 4-х типов (подробности тут).
URL-кнопки
Итак, инлайн-кнопки. Что это такое? Это специальные объекты, которые “цепляются” к конкретным сообщениям и распространяют своё действие, в общем случае, только на них. Делятся такие кнопки на три типа: URL-кнопки, Callback-кнопки и Switch-кнопки. Самыми простыми являются кнопки-ссылки (URL). Как видно из названия, их цель — просто перекидывать пользователей по определенным веб-адресам. Давайте сразу напишем обработчик, который будет на любое сообщение отвечать каким-либо текстом и предложением перейти, например, на Яндекс.
@bot.message_handler(content_types=["text"]) def default_test(message): keyboard = types.InlineKeyboardMarkup() url_button = types.InlineKeyboardButton(text="Перейти на Яндекс", url="https://ya.ru") keyboard.add(url_button) bot.send_message(message.chat.id, "Привет! Нажми на кнопку и перейди в поисковик.", reply_markup=keyboard)
Инлайн-клавиатура представляет собой объект InlineKeyboardMarkup , а каждая инлайн-кнопка – это объект InlineKeyboardButton . Чтобы получилась URL-кнопка, нужно указать значения параметров text (текст на кнопке) и url (валидный веб-адрес). В результате бот пришлет нам такое сообщение (см. рис.). В целях обеспечения безопасности, перед переходом по URL-кнопкам появляется всплывающее окно, в котором видна ссылка целиком.
Callback-кнопки и редактирование сообщений
Прежде, чем мы перейдем к другим кнопкам, давайте познакомимся с функциями редактирования сообщений, коих тоже три: editMessageText (редактирование текста), editMessageCaption (редактирование подписи к медиа) и editMessageReplyMarkup (редактирование инлайн-клавиатуры). В рамках этого урока рассмотрим только первую функцию, остальные работают аналогично и предлагаются для самостоятельного изучения.
Чтобы отредактировать сообщение, нам надо знать, про какое именно идёт речь. В случае, если оно было отправлено самим ботом, идентификаторами служит связка chat_id + message_id . Но если сообщение было отправлено в инлайн-режиме, то ориентироваться надо по параметру inline_message_id .
И вот теперь вернемся к нашим баранам кнопкам. На очереди – Callback. Это, на мой взгляд, самая крутая фича нового обновления. Колбэк-кнопки позволяют выполнять произвольные действия по их нажатию. Всё зависит от того, какие параметры каждая кнопка в себе несёт. Соответственно, все нажатия будут приводить к отправке боту объекта CallbackQuery , содержащему поле data , в котором написана некоторая строка, заложенная в кнопку, а также либо объект Message , если сообщение отправлено ботом в обычном режиме, либо поле inline_message_id , если сообщение отправлено в инлайн-режиме.
Приведу пример, после которого все вопросы должны отпасть: пусть, например, если сообщение отправлено ботом в обычном режиме, то нажатие на кнопку заменит текст сообщения на “Пыщь”, если в инлайн – то “Бдыщь”. При этом в обоих случаях значение callback_data будет равно test . Что для этого нужно сделать: во-первых, написать простейший хэндлер для всех входящих сообщений, во-вторых, написать простейший хэндлер для инлайн-сообщений, в-третьих, написать простейший хэндлер для колбэка, который определит, из какого режима пришло сообщение.
# Обычный режим @bot.message_handler(content_types=["text"]) def any_msg(message): keyboard = types.InlineKeyboardMarkup() callback_button = types.InlineKeyboardButton(text="Нажми меня", callback_data="test") keyboard.add(callback_button) bot.send_message(message.chat.id, "Я – сообщение из обычного режима", reply_markup=keyboard) # Инлайн-режим с непустым запросом @bot.inline_handler(lambda query: len(query.query) > 0) def query_text(query): kb = types.InlineKeyboardMarkup() # Добавляем колбэк-кнопку с содержимым "test" kb.add(types.InlineKeyboardButton(text="Нажми меня", callback_data="test")) results = [] single_msg = types.InlineQueryResultArticle( style=color:#a31515>"1", title="Press me", input_message_content=types.InputTextMessageContent(message_text="Я – сообщение из инлайн-режима"), reply_markup=kb ) results.append(single_msg) bot.answer_inline_query(query.id, results) # В большинстве случаев целесообразно разбить этот хэндлер на несколько маленьких @bot.callback_query_handler(func=lambda call: True) def callback_inline(call): # Если сообщение из чата с ботом if call.message: if call.data == "test": bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Пыщь") # Если сообщение из инлайн-режима elif call.inline_message_id: if call.data == "test": bot.edit_message_text(inline_message_id=call.inline_message_id, text="Бдыщь") if __name__ == '__main__': bot.infinity_polling()
Запускаем бота, отправляем инлайн-сообщение, которое, в свою очередь, вызовет обычное:
Нажмем на обе кнопки, результат правильный:
После проверки
Таким образом, callback-кнопки – это очень мощный инструмент для взаимодействия пользователей с ботом, а редактирование сообщений дополнительно помогает в этом. Более того, нажатие на колбэк-кнопку может дополнительно тригернуть либо уведомление в верхней части экрана, либо всплывающее окно. Покажу первый вариант. Пускай помимо изменения сообщения на “Пыщь”, аналогичное слово показывается уведомлением. Для этого перепишем первое if-условие в хендлере колбэков:
if call.message: if call.data == "test": bot.edit_message_text(chat_id=call.message.chat.id, message_id=call.message.message_id, text="Пыщь") bot.answer_callback_query(callback_query_id=call.id, show_alert=False, text="Пыщь!")
Результат – на скриншоте. Попробуйте, кстати, изменить аргумент show_alert на True и посмотрите, что получится.
Всплывающее уведомление
Switch-кнопки
Наконец, остался последний тип кнопок — Switch (переключатель). Они нужны, чаще всего, для обучения пользователей работе с ботом в инлайн-режиме. Чтобы активировать сделать кнопку такого типа, нужно указать аргумент switch_inline_query либо пустой, либо с каким-либо текстом. В последнем случае этот текст будет сразу подставлен в поле ввода, например, для показа демонстрации инлайна. Как вообще работает такая кнопка? При нажатии на неё Telegram предложит выбрать чат, после чего подставит в поле ввода ник вашего бота и (если есть), текст, указанный вами в аргументе switch_inline_query . Давайте попробуем так сделать. Добавим кнопку, которая будет перенаправлять пользователя в какой-либо чат и предлагать в инлайн-режиме запрос “Telegram”. Код всего хендлера выглядит вот так:
@bot.message_handler(content_types=["text"]) def any_msg(message): keyboard = types.InlineKeyboardMarkup() switch_button = types.InlineKeyboardButton(text="Нажми меня", switch_inline_query="Telegram") keyboard.add(switch_button) bot.send_message(message.chat.id, "Я – сообщение из обычного режима", reply_markup=keyboard)
Теперь, если мы нажмем на кнопку и выберем чат, вот что получится:
Итак, в этом уроке мы познакомились с новыми кнопками в Telegram Bot API, научились переписывать историю редактировать сообщения и отправлять небольшие уведомления по нажатию. В следующий раз продолжим изучать новые возможности для ботов. А исходники к этому уроку можно найти в этом репозитории.