Перейти к содержанию

KTB-16 Разработать кнопки и отправлять их пользователю в телеграм

📝 Описание

Необходимо реализовать функциональность создания и отправки кнопок в сообщениях Telegram-бота. Процесс включает следующие этапы:

  • Формирование объектов кнопок в коде на основе примеров из документации Telegram API.
  • Сериализация объектов кнопок в формат JSON для последующей отправки.
  • Отправка сообщений с кнопками с использованием метода sendMessage и параметра reply_markup.
  • Обработка обратных вызовов (callback) при нажатии пользователем на кнопки.

// видео


Ключевые моменты реализации

  1. Использование метода sendMessage для отправки кнопок:
    • Метод sendMessage применяется для отправки сообщений в чат.
    • Опциональный параметр reply_markup позволяет добавить кнопки к сообщениям.
    • Существуют два типа кнопок:
      • Inline-клавиатура: кнопки отображаются внутри сообщения.
      • Reply-клавиатура: кнопки отображаются в интерфейсе клиента.
  2. Создание объекта кнопок и сериализация в JSON:
    • Формирование объекта кнопок на основе примеров из документации Telegram API.
    • Преобразование (сериализация) объекта кнопок в JSON-строку для передачи в запросе.
    • Пример создания JSON-объекта с кнопками для корректной передачи данных.
  3. Создание и отправка POST-запроса с телом в формате JSON:
    • Разработка метода sendMenu() для отправки меню с кнопками пользователю.
    • Использование OkHttpClient для создания и отправки POST-запроса.
  4. Обработка ответа на нажатие кнопок (callback):
    • Получение объекта callback_query, представляющего ответ от нажатой кнопки пользователем.
    • Использование поля data в объекте callback_query для определения нажатой кнопки и выполнение соответствующего действия на основе нажатой кнопки.

🗒 Задание

  1. Добавить отображение основного меню с кнопками в ответ на команду /start, используя созданный метод sendMenu().
  2. Переместить класс Word в файл трейнера (LearnWordsTrainer.kt), если это ещё не было сделано ранее. Для соблюдения базовых архитектурных рекомендаций нужно переиспользовать ключевую сущность.
  3. Создать и инициализировать переменную trainer экземпляром класса LearnWordsTrainer перед циклом, в котором происходит получение обновлений от Telegram Bots API. Это потребуется для дальнейшей реализации основного функционала бота.

❌ Ошибки

✏️ Комментарии

Если в каждом методе внутриTelegramBotService создает новый client

HttpClient нужно сделать один общий для всего сервиса

На случай если студент не вынес код в TelegramBotService

Давай разместим все методы для работы с `Telegram API` внутри класса `TelegramBotService`. Тогда внутри `TelegramBotService` можно сделать поле `botToken`, чтобы не передавать в каждый метод. А также вынести в константу `https://api.telegram.org/bot`.
Давай вынесем `"learn_words_clicked"`  в константы `"statistics_clicked"` из тела `sendMenuBody`

⌨️ Решение

import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.charset.StandardCharsets

const val TELEGRAM_BASE_URL = "https://api.telegram.org"
const val LEARN_WORDS_CLICKED = "learn_words_clicked"
const val STATISTICS_CLICKED = "statistics_clicked"

class TelegramBotService(
    private val botToken: String,
) {
    private val client: HttpClient = HttpClient.newHttpClient()

    fun getUpdates(updateId: Int): String {
        val urlGetUpdates = "$TELEGRAM_BASE_URL/bot$botToken/getUpdates?offset=$updateId"
        val httpRequest: HttpRequest = HttpRequest.newBuilder().uri(URI.create(urlGetUpdates)).build()
        val response: HttpResponse<String> = client.send(httpRequest, HttpResponse.BodyHandlers.ofString())
        return response.body()
    }

    fun sendMessage(chatId: Int, text: String): String {
        // энкодинг нужен, потому что параметр передается через query GET-запроса
        val encodedText = URLEncoder.encode(text, StandardCharsets.UTF_8.toString())
        val urlSendMessage = "$TELEGRAM_BASE_URL/bot$botToken/sendMessage?chat_id=$chatId&text=$encodedText"
        val httpRequest: HttpRequest = HttpRequest.newBuilder().uri(URI.create(urlSendMessage)).build()

        /* Рефакторинг в KTB-16 (общий клиент и возврат body у метода)
        val client: HttpClient = HttpClient.newHttpClient()
        client.send(httpRequest, HttpResponse.BodyHandlers.ofString())
        */

        val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString())
        return response.body()
    }

    fun sendMenu(chatId: Int): String {
        val urlSendMessage = "$TELEGRAM_BASE_URL/bot$botToken/sendMessage"
        val sendMenuBody = """
            {
                "chat_id": $chatId,
                "text": "Основное меню",
                "reply_markup": {
                    "inline_keyboard": [
                        [
                            {
                                "text": "Learn words",
                                "callback_data": "$LEARN_WORDS_CLICKED"
                            },
                            {
                                "text": "Statistics",
                                "callback_data": "$STATISTICS_CLICKED"
                            }
                        ]
                    ]
                }
            }
        """.trimIndent()

        val httpRequest: HttpRequest = HttpRequest.newBuilder().uri(URI.create(urlSendMessage))
            .header("Content-type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(sendMenuBody))
            .build()
        val response = client.send(httpRequest, HttpResponse.BodyHandlers.ofString())
        return response.body()
    }

}
fun main(args: Array<String>) {

    val botToken = args[0]
    var updateId = 0
    val telegramBotService = TelegramBotService(botToken)
    val trainer = LearnWordsTrainer()

    val regexFindText: Regex = "\"text\":\"(.+?)\"".toRegex()
    val regexFindUpdateId = "\"update_id\":(\\d+)".toRegex()
    val regexFindChatId = "\"chat\":\\{\"id\":(.+?)\"".toRegex()
    val regexFindData = "\"data\":\"(.+?)\"".toRegex()

    while (true) {
        Thread.sleep(2000)

        // KTB-15 (сократили)
        // val updates = getUpdates(botToken, updateId)
        val updates = telegramBotService.getUpdates(updateId)
        println(updates)

        //  ?: continue обязательно, чтобы пропускать, если нет updateId. тк должен присутствовать в каждом ответе
        val updateIdInt = regexFindUpdateId.find(updates)?.groups?.get(1)?.value?.toIntOrNull() ?: continue
        updateId = updateIdInt + 1
        val text = regexFindText.find(updates)?.groups?.get(1)?.value

        //  ?: continue обязательно аналогично
        val chatId = regexFindChatId.find(updates)?.groups?.get(1)?.value?.toIntOrNull() ?: continue
        val data = regexFindData.find(updates)?.groups?.get(1)?.value
        updateId = updateIdInt + 1

        if (text.equals("/start", true)) {
            telegramBotService.sendMenu(chatId)
        }

        if (data.equals(STATISTICS_CLICKED, true)) {
            telegramBotService.sendMessage(chatId, "Выучено 10 из 10 слов")
        }
    }
}

/* Перенесено в отдельный класс TelegramBotService в KTB-15
fun getUpdates(botToken: String, updateId: Int): String {
    val urlGetUpdates = "https://api.telegram.org/bot$botToken/getUpdates?offset=$updateId"
    val client: HttpClient = HttpClient.newHttpClient()
    val httpRequest: HttpRequest = HttpRequest.newBuilder().uri(URI.create(urlGetUpdates)).build()
    val response: HttpResponse<String> = client.send(httpRequest, HttpResponse.BodyHandlers.ofString())
    return response.body()
}
*/