Перейти к публикации

decl1998

Делаем кнопки в телеграмме на python чаcть 2

Рекомендованные сообщения

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

Login- и Pay-кнопки в книге рассматриваться не будут вообще.

Самые простые инлайн-кнопки относятся к типу URL, т.е. «ссылка». Поддерживаются только протоколы HTTP(S) и tg://

Python:

@dp.message_handler(commands="inline_url")
async def cmd_inline_url(message: types.Message):
    buttons = [
        types.InlineKeyboardButton(text="GitHub", url="https://github.com"),
        types.InlineKeyboardButton(text="Оф. канал Telegram", url="tg://resolve?domain=telegram")
    ]
    keyboard = types.InlineKeyboardMarkup(row_width=1)
    keyboard.add(*buttons)
    await message.answer("Кнопки-ссылки", reply_markup=keyboard)

А если хотите обе кнопки в ряд, то уберите row_width=1 (тогда будет использоваться значение по умолчанию 3).

С URL-кнопками больше обсуждать, по сути, нечего, поэтому перейдём к гвоздю сегодняшней программы — Callback-кнопкам.
Это очень мощная штука, которую вы можете встретить практически везде.
Кнопки-реакции у постов (лайки), меню у @BotFather и т.д. Суть в чём: у колбэк-кнопок есть специальное значение (data), по которому ваше приложение опознаёт, что нажато и что надо сделать. И выбор правильного data очень важен! Стоит также отметить, что, в отличие от обычных кнопок, нажатие на колбэк-кнопку позволяет сделать практически что угодно, от заказа пиццы до перезагрузки сервера.

Напишем хэндлер, который по команде /random будет отправлять сообщение с колбэк-кнопкой:

Python:

@dp.message_handler(commands="random")
async def cmd_random(message: types.Message):
    keyboard = types.InlineKeyboardMarkup()
    keyboard.add(types.InlineKeyboardButton(text="Нажми меня", callback_data="random_value"))
    await message.answer("Нажмите на кнопку, чтобы бот отправил число от 1 до 10", reply_markup=keyboard)

Но как же обработать нажатие? Если раньше мы использовали message_handler для обработки входящих сообщений, то теперь будем использовать callback_query_handler для обработки колбэков.
Ориентироваться будем на «значение» кнопки, т.е. на её data:

Python:

@dp.callback_query_handler(text="random_value")
async def send_random_value(call: types.CallbackQuery):
    await call.message.answer(str(randint(1, 10)))

Несмотря на то, что параметр кнопки callback_data, а значение data лежит в одноимённом поле data объекта CallbackQuery, собственный фильтр aiogram называется text.

uacaua-png.252

Ой, а что это за часики? Оказывается, сервер Telegram ждёт от нас подтверждения о доставке колбэка, иначе в течение 30 секунд будет показывать специальную иконку.
Чтобы скрыть часики, нужно вызвать метод answer() у колбэка (или использовать метод API answer_callback_query()).
В общем случае, в метод answer() можно ничего не передавать, но можно вызвать специальное окошко (всплывающее сверху или поверх экрана):

Python:

@dp.callback_query_handler(text="random_value")
async def send_random_value(call: types.CallbackQuery):
    await call.message.answer(str(randint(1, 10)))
    await call.answer(text="Спасибо, что воспользовались ботом!", show_alert=True)
    # или просто await call.answer()

kpcukpukpukp-png.253
В функции send_random_value мы вызывали метод answer() не у message, а у call.message.
Это связано с тем, что колбэк-хэндлеры работают не с сообщениями (тип Message), а с колбэками (тип CallbackQuery), у которого другие поля, и само сообщение — всего лишь его часть.
Учтите также, что message — это сообщение, к которому была прицеплена кнопка (т.е. отправитель такого сообщения — сам бот).
Если хотите узнать, кто нажал на кнопку, смотрите поле from (в вашем коде это будет call.from_user, т.к. слово from зарезервировано в Python)

Перейдём к примеру посложнее. Пусть пользователю предлагается сообщение с числом 0, а внизу три кнопки: +1, -1 и Подтвердить.
Первыми двумя он может редактировать число, а последняя удаляет всю клавиатуру, фиксируя изменения.
Хранить значения будем в памяти в словаре (про конечные автоматы поговорим как-нибудь в другой раз).

Python:

# Здесь хранятся пользовательские данные.
# Т.к. это словарь в памяти, то при перезапуске он очистится
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")
    ]
    # Благодаря row_width=2, в первом ряду будет две кнопки, а оставшаяся одна
    # уйдёт на следующую строку
    keyboard = types.InlineKeyboardMarkup(row_width=2)
    keyboard.add(*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_handler(commands="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_handler(Text(startswith="num_"))
async def callbacks_num(call: types.CallbackQuery):
    # Получаем текущее значение для пользователя, либо считаем его равным 0
    user_value = user_data.get(call.from_user.id, 0)
    # Парсим строку и извлекаем действие, например `num_incr` -> `incr`
    action = call.data.split("_")[1]
    if action == "incr":
        user_data[call.from_user.id] = user_value+1
        await update_num_text(call.message, user_value+1)
    elif action == "decr":
        user_data[call.from_user.id] = user_value-1
        await update_num_text(call.message, user_value-1)
    elif action == "finish":
        # Если бы мы не меняли сообщение, то можно было бы просто удалить клавиатуру
        # вызовом await call.message.delete_reply_markup().
        # Но т.к. мы редактируем сообщение и не отправляем новую клавиатуру,
        # то она будет удалена и так.
        await call.message.edit_text(f"Итого: {user_value}")
    # Не забываем отчитаться о получении колбэка
    await call.answer()

И, казалось бы, всё работает:

l03_7-png.254

Но теперь представим, что ушлый пользователь сделал следующее: вызвал команду /numbers (значение 0), увеличил значение до 1, снова вызвал /numbers (значение сбросилось до 0) и отредактировал нажал кнопку "+1" на первом сообщении.
Что произойдёт?
Бот по-честному отправит запрос на редактирование текста со значением 1, но т.к. на том сообщении уже стоит цифра 1, то Bot API вернёт ошибку, что старый и новый тексты совпадают, а бот словит исключение: aiogram.utils.exceptions.MessageNotModified: 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

l03_8-png.255

С этой ошибкой вы, скорее всего, будете поначалу часто сталкиваться, пытаясь редактировать сообщения.
Но, в действительности, решается проблема очень просто: мы проигнорируем исключение MessageNotModified.

Python:

# from aiogram.utils.exceptions import MessageNotModified
# from contextlib import suppress

async def update_num_text(message: types.Message, new_value: int):
    with suppress(MessageNotModified):
        await message.edit_text(f"Укажите число: {new_value}", reply_markup=get_keyboard())

Если теперь вы попробуете повторить пример выше, то указанное исключение в этом блоке кода бот просто-напросто проигнорирует.

Фабрика колбэков
В aiogram существует т.н. фабрика колбэков. Вы создаёте объект CallbackData, указываете ему префикс и произвольное количество доп. аргументов, которые в дальнейшем указываете при создании колбэка для кнопки.
Например, рассмотрим следующий объект:

Python:

# from aiogram.utils.callback_data import CallbackData
cb= CallbackData("post", "id", "action")

Тогда при создании кнопки вам надо указать её параметры так:
Python:

button =  types.InlineKeyboardButton(
    text="Лайкнуть",
    callback_data=cb.new(id=5, action="like")
)

В примере выше в кнопку запишется callback_data, равный post:5:like, а хэндлер на префикс post будет выглядеть так:

Python:

@dp.callback_query_handler(cb.filter())
async def callbacks(call: types.CallbackQuery, callback_data: dict):
    post_id = callback_data["id"]
    action = callback_data["action"]

В предыдущем примере с числами мы грамотно выбрали callback_data, поэтому смогли легко запихнуть все обработчики в один хэндлер.
Но можно логически разнести обработку инкремента и декремента от обработки нажатия на кнопку "Подтвердить".
Для этого в фильтре фабрики можно указать желаемые значения какого-либо параметра. Давайте перепишем наш пример с использованием фабрики:

Python:

# fabnum - префикс, action - название аргумента, которым будем передавать значение
callback_numbers = CallbackData("fabnum", "action")


def get_keyboard_fab():
    buttons = [
        types.InlineKeyboardButton(text="-1", callback_data=callback_numbers.new(action="decr")),
        types.InlineKeyboardButton(text="+1", callback_data=callback_numbers.new(action="incr")),
        types.InlineKeyboardButton(text="Подтвердить", callback_data=callback_numbers.new(action="finish"))
    ]
    keyboard = types.InlineKeyboardMarkup(row_width=2)
    keyboard.add(*buttons)
    return keyboard


async def update_num_text_fab(message: types.Message, new_value: int):
    with suppress(MessageNotModified):
        await message.edit_text(f"Укажите число: {new_value}", reply_markup=get_keyboard_fab())


@dp.message_handler(commands="numbers_fab")
async def cmd_numbers(message: types.Message):
    user_data[message.from_user.id] = 0
    await message.answer("Укажите число: 0", reply_markup=get_keyboard_fab())


@dp.callback_query_handler(callback_numbers.filter(action=["incr", "decr"]))
async def callbacks_num_change_fab(call: types.CallbackQuery, callback_data: dict):
    user_value = user_data.get(call.from_user.id, 0)
    action = callback_data["action"]
    if action == "incr":
        user_data[call.from_user.id] = user_value + 1
        await update_num_text_fab(call.message, user_value + 1)
    elif action == "decr":
        user_data[call.from_user.id] = user_value - 1
        await update_num_text_fab(call.message, user_value - 1)
    await call.answer()


@dp.callback_query_handler(callback_numbers.filter(action=["finish"]))
async def callbacks_num_finish_fab(call: types.CallbackQuery):
    user_value = user_data.get(call.from_user.id, 0)
    await call.message.edit_text(f"Итого: {user_value}")
    await call.answer()

На этом глава про кнопки окончена, но про некоторые других их виды мы поговорим в следующих главах.

Поделиться сообщением


Ссылка на сообщение

Создайте аккаунт или войдите в него для комментирования

Вы должны быть пользователем, чтобы оставить комментарий

Создать аккаунт

Зарегистрируйтесь для получения аккаунта. Это просто!

Зарегистрировать аккаунт

Войти

Уже зарегистрированы? Войдите здесь.

Войти сейчас

×
×
  • Создать...