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

KTB-18 Реализовать функционал изучения слов в боте

📝 Описание

Добавить условие для обработки нажатия кнопки "Изучить слова" (аналогично обработке кнопки "Статистика").

Внутри этого условия:

  • Получить следующий вопрос из объекта trainer.
  • Если вопрос отсутствует, отправить пользователю сообщение: "Вы выучили все слова в базе".
  • В противном случае, вызвать функцию sendQuestion() для отправки вопроса пользователю.

Детали реализации

Необходимо реализовать функционал изучения слов в приложении. Для этого требуется:

  • Создать константу CALLBACK_DATA_ANSWER_PREFIX со значением 'answer_'.
  • Разработать функцию для отправки сущности “вопроса” пользователю. Эта функция должна принимать параметры chatId и объект Question.

Внутри функции необходимо сформировать тело запроса для метода sendMessage с учётом следующих требований:

  • Поле text: указать корректный ответ на английском языке.
  • Поле inline_keyboard: создать JSON-объект с вариантами ответов. При этом:
    • Поле text каждой кнопки должно содержать перевод варианта ответа.
    • Поле callback_data каждой кнопки должно быть конкатенацией константы CALLBACK_DATA_ANSWER_PREFIX и индекса варианта ответа.

Для получения индекса каждого варианта ответа рекомендуется использовать функции forEachIndexed или mapIndexed.

🗒 Задание

  1. Создать константу CALLBACK_DATA_ANSWER_PREFIX со значением 'answer_'.
  2. Реализовать функцию для отправки вопроса пользователю с вариантами ответов, учитывая требования к формированию тела запроса для sendMessage.
  3. Добавить условие для обработки нажатия кнопки "Изучить слова":
    1. Получить следующий вопрос из объекта trainer.
    2. Если “вопрос” отсутствует, отправить сообщение: "Все слова в словаре выучены".
    3. Иначе, отправить “вопрос” пользователю, вызвав функцию sendQuestion().
  4. Вынести логику из предыдущего шага в функцию с такой сигнатурой:
fun checkNextQuestionAndSend(
        trainer: LearnWordsTrainer, 
        telegramBotService: TelegramBotService, 
        chatId: Int
)

❌ Ошибки

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

На данном этапе чаще всего ошибки происходят при формировании кнопок с вариантами ответа. Можно предложить студенту двигаться по шагам. Сначала сделать вариант без mapIndexed {} и потом улучшить. Например:

Попробуй сначала декомпозировать. Отделить код который составляет часть json для отображения кнопок от остального кода:

val variantsString = """
{
    "text": "${question.variants[0].translate}",
    "callback_data": "$CALLBACK_DATA_ANSWER_PREFIX${variants[0]}"
},
{
    "text": "${question.variants[1].translate}",
    "callback_data": "$CALLBACK_DATA_ANSWER_PREFIX${variants[1]}"
},
{
    "text": "${question.variants[2].translate}",
    "callback_data": "$CALLBACK_DATA_ANSWER_PREFIX${variants[2]}"
},
{
    "text": "${question.variants[3].translate}",
    "callback_data": "$CALLBACK_DATA_ANSWER_PREFIX${variants[3]}"
}
"""

val sendQuestionBody = """
{
    "chat_id": $chatId,
    "text": "${question.correctAnswer.original}",
    "reply_markup": {
        "inline_keyboard": [
            [
                $variantsString,
                {
                    "text": "Меню",
                    "callback_data": "$MENU"
                }
            ]
        ]
    }
}
""".trimIndent()`

println(sendQuestionBody)

А теперь обрати внимание, что `{ "text": "${question.variants[3].translate}", "callback_data": "$CALLBACK_DATA_ANSWER_PREFIX${variants[3]}" }` повторяется на каждый variant из `question.variants` и соединяется запятой. Мы можем сделать данный код компактнее и универсальнее.

Для этого нам надо каждый `variant` смапить в строку, а вместо цифры использовать индекс: 

val variantsString = question.variants
    .mapIndexed { index, word -> 
        """
            {
                "text": "${word.translate}",
                "callback_data": "$CALLBACK_DATA_ANSWER_PREFIX${index}" 
            }
        """.trimIndent() 
    }
    .joinToString(separator = ",")

Еще часто не создают переменную question в checkNextQuestionAndSend(...), а вызывают trainer.getNextQuestion() каждый раз:

fun checkNextQuestionAndSend(trainer: LearnWordsTrainer, service: TelegramBotService, chatId: Int) {
    if (question == trainer.getNextQuestion()) { // !!! ГРУБАЯ ОШИБКА
        service.sendMessage(chatId, "Вы выучили все слова в базе")
    } else {
        service.sendQuestion(chatId, trainer.getNextQuestion()) // !!! ГРУБАЯ ОШИБКА
    }
}

Это грубая ошибка, ведь getNextQuestion() приводит к генерации нового вопроса. Коммент может быть таким:

Так делать нельзя, ведь `getNextQuestion()` создает нам новый вопрос. Давай создадим переменную `question` перед if.

⌨️ Решение

// Telegram.kt
fun checkNextQuestionAndSend(trainer: LearnWordsTrainer, service: TelegramBotService, chatId: Int) {
    val question = trainer.getNextQuestion()
    if (question == null) {
        service.sendMessage(chatId, "Вы выучили все слова в базе")
    } else {
        service.sendQuestion(chatId, question)
    }
}

// TelegramBotService.kt
fun sendQuestion(chatId: Int, question: Question): String {
    val urlSendMessage = "$TELEGRAM_BASE_URL/bot$botToken/sendMessage"
    val keyboardLayout = question.variants
        .mapIndexed { index: Int, word: Word -> "{ \"text\": \"${word.translate}\", \"callback_data\": \"$CALLBACK_DATA_ANSWER_PREFIX$index\" }" }
        .joinToString(separator = ",")
    println(keyboardLayout)
    val sendMenuBody = """
    {
      "chat_id": $chatId,
      "text": "${question.correctAnswer.original}",
      "reply_markup": {
        "inline_keyboard": [[ $keyboardLayout ]]
      }
    }            
    """.trimIndent()
    val request = HttpRequest.newBuilder(URI.create(urlSendMessage))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(sendMenuBody))
        .build()
    val response = client.send(request, HttpResponse.BodyHandlers.ofString())
    return response.body()
}