Статьи
Утилиты Telegram YouTube Отзывы

Inline-кнопки в telegram-боте

Исходники

29 декабря 2023

Тэги: Kotlin, Spring Boot, руководство.

Содержание

  1. Добавляем поддержку обратных вызовов
  2. Реализуем обработку inline кнопок
  3. Заключение

В предыдущей статье Telegram-бот на Spring Boot мы написали telegram-бота на Kotlin и Spring Boot. Мы рассмотрели, как добавляются простые команды, команды с параметрами, а также научились отображать простые кнопки в клиентском приложении telegram.

Но обычные кнопки являются не более чем заранее подготовленными текстовыми сообщениями пользователя. Они не несут никакого контекста и потому не подходят для более сложных сценариев взаимодействия. Однако пришла пора добавить в наш бот чуть больше интерактива!

Добавляем поддержку обратных вызовов

Предположим, вы решили написать онлайн-квиз в виде Telegram-бота. Бот должен выдать вопрос и какие-то варианты ответа к нему. Если это делать с помощью обычных кнопок, то довольно сложно сопоставить ответ пользователя с конкретным вопросом, т.к. обычные кнопки не хранят контекст. Но есть встраиваемые (inline) кнопки, которые относятся непосредственно к сообщению. Они добавляют гораздо больше интерактива и именно их вы видели, когда регистрировали бота в BotFather.

inline-кнопки в Bot Father

Встроенные кнопки взаимодействуют с ботом в фоновом режиме при помощи обратных вызовов (callback). Поэтому имеет смысл создать отдельный набор классов и интерфейсов для их поддержки.

HandlerName – это enum с именами обратных вызовов. Аналог CommandName.

enum class HandlerName(val text: String) {
    QUIZ_ANSWER("quiz_answer"),
}

В данном случае добавим имя для обработки ответа на вопросы из нашего квиза.

Далее создадим базовый интерфейс CallbackHandler (аналог BotCommand).

interface CallbackHandler {

    val name: HandlerName

    fun processCallbackData(absSender: AbsSender, callbackQuery: CallbackQuery, arguments: List<String>)
}

Теперь доработаем основной класс DevmarkBot для поддержки обратных вызовов. Аналогично командам, будем передавать в конструктор все реализации только что созданного интерфейса и в секции init будем строить маппинг имени обработчика на его реализацию с помощью метода associateBy(). Этот маппинг будем хранить в поле handlerMapping.

@Component
class DevmarkBot(
    commands: Set<BotCommand>,
    callbackHandlers: Set<CallbackHandler>,
    @Value("\${telegram.token}")
    token: String,
) : TelegramLongPollingCommandBot(token) {
    // ...
    private lateinit var handlerMapping: Map<String, CallbackHandler>

    init {
        registerAll(*commands.toTypedArray())
        handlerMapping = callbackHandlers.associateBy { it.name.text }
    }
    // ...
}

Также потребуется добавить новую ветку в метод processNonCommandUpdate():

override fun processNonCommandUpdate(update: Update) {
    if (update.hasMessage()) {
        // обработка обычных сообщений
    } else if (update.hasCallbackQuery()) {
        val callbackQuery = update.callbackQuery
        val callbackData = callbackQuery.data

        val callbackQueryId = callbackQuery.id
        execute(AnswerCallbackQuery(callbackQueryId))

        val callbackArguments = callbackData.split("|")
        val callbackHandlerName = callbackArguments.first()

        handlerMapping.getValue(callbackHandlerName)
            .processCallbackData(
                this,
                callbackQuery,
                callbackArguments.subList(1, callbackArguments.size)
            )
    }
}

Здесь мы добавили новую проверку hasCallbackQuery(), внутри которой извлекаем контекст callbackData. Технически он представляет собой строку и вы можете поместить туда всё что угодно. Это именно тот самый контекст, которого нам не хватает в функционале обычных кнопок.

Перед началом обработки запроса нам нужно отправить клиентскому приложению AnswerCallbackQuery. Мы как бы говорим, что запрос принят и мы начали его обработку. Если этого не сделать, то клиент будет видеть анимацию обработки запроса и подумает, что наше приложение «зависло».

Далее начинаем извлекать информацию из callbackData. Формат может быть любым, но при этом должен быть единым для всех ваших callback-ов. Я здесь разделяю параметры вертикальной чертой, причём первым параметром всегда ставлю текстовое имя из HandlerName. По этому имени я извлекаю нужный обработчик из handlerMapping и передаю все остальные аргументы кроме первого внутрь этого обработчика.

Реализуем обработку inline кнопок

Теперь инфраструктура нашего бота полностью поддерживает обработку inline-кнопок. Сначала создадим обработчик для команды /quiz, который будет выводить вопрос викторины и варианты ответов. В нашем примере это хардкод, а в реальности мы считывали бы эти вопросы из базы данных.

@Component
class QuizCommand: BotCommand(CommandName.QUIZ.text, "") {
    override fun execute(absSender: AbsSender, user: User, chat: Chat, arguments: Array<out String>) {
        val callback = HandlerName.QUIZ_ANSWER.text
        absSender.execute(
            createMessageWithInlineButtons(
                chat.id.toString(),
                "Как называется ближайшая к Солнцу планета?",
                listOf(
                    listOf("$callback|a" to "Земля", "$callback|b" to "Меркурий"),
                    listOf("$callback|c" to "Плутон", "$callback|d" to "Юпитер"),
                )
            )
        )
    }
}

Здесь мы используем метод createMessageWithInlineButtons(), который принимает не просто список списков строк, а принимает пары ключ-значение. Где ключом является то, что будет передано в callbackData, а значением – надпись на кнопке.

В данном случае callbackData формируется как имя обработки обратного вызова и буква варианта ответа, разделённые вертикальной чертой.

Утилитарные методы формирования inline-кнопок похожи на уже рассмотренные ранее.

fun createMessageWithInlineButtons(chatId: String, text: String, inlineButtons: List<List<Pair<String, String>>>) =
    createMessage(chatId, text)
        .apply {
            replyMarkup = getInlineKeyboard(inlineButtons)
        }

fun getInlineKeyboard(allButtons: List<List<Pair<String, String>>>): InlineKeyboardMarkup =
    InlineKeyboardMarkup().apply {
        keyboard = allButtons.map { rowButtons ->
            rowButtons.map { (data, buttonText) ->
                InlineKeyboardButton().apply {
                    text = buttonText
                    callbackData = data
                }
            }
        }
    }

Теперь осталось написать обработчик QuizAnswerHandler для проверки правильности ответа.

@Component
class QuizAnswerHandler : CallbackHandler {

    override val name: HandlerName = HandlerName.QUIZ_ANSWER

    override fun processCallbackData(
        absSender: AbsSender,
        callbackQuery: CallbackQuery,
        arguments: List<String>
    ) {

        val chatId = callbackQuery.message.chatId.toString()

        absSender.execute(
            EditMessageReplyMarkup(
                chatId,
                callbackQuery.message.messageId,
                callbackQuery.inlineMessageId,
                getInlineKeyboard(emptyList())
            )
        )

        if (arguments.first() == "b") {
            absSender.execute(createMessage(chatId, "Абсолютно верно!"))
        } else {
            absSender.execute(createMessage(chatId, "К сожалению, Вы ошиблись..."))
        }
    }
}

Здесь мы сначала отправляем EditMessageReplyMarkup с помощью absSender. Он убирает кнопки с вариантами ответа из конкретного сообщения, т.к. если один из вариантов выбран, то кнопки больше не нужны. Мы просто передаём в getInlineKeyboard() пустой список. Сходным образом можно отредактировать исходное сообщение. Например, зафиксировать вариант ответа.

Далее извлекаем из аргументов обратного вызова выбранный вариант ответа. Помним, что первым аргументом изначально было имя обратного вызова, но мы его отсекаем на уровне DevmarkBot. Проверяем вариант и отправляем новое сообщение в зависимости от правильности ответа.

inline-кнопки в telegram-боте

В реальном квизе скорее всего потребуется также передавать ещё и номер вопроса.

Заключение

Как видите, Spring Boot позволяет запустить чат-бот с минимальным количеством усилий. Я постарался предложить простую, но вполне рабочую заготовку для типового telegram-бота на основе собственного практического опыта. Вам же остаётся только добавить обработчики команд и обратных вызовов согласно вашим требованиям.

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



Комментарии

Добавить комментарий

×

devmark.ru