JavaRush/Java блог/Random/Создаем телеграм-бота с использованием Spring Boot Pt.3: ...
Whiskels
41 уровень

Создаем телеграм-бота с использованием Spring Boot Pt.3: Quiz Bot

Статья из группы Random
участников
ЧАСТЬ 1 ЧАСТЬ 2 К сожалению, первая версия статьи не пролезла в лимит символов, так что заканчиваю здесь: QuizHandler:
package com.whiskels.telegram.bot.handler;

import com.whiskels.telegram.bot.State;
import com.whiskels.telegram.model.Question;
import com.whiskels.telegram.model.User;
import com.whiskels.telegram.repository.JpaQuestionRepository;
import com.whiskels.telegram.repository.JpaUserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.methods.PartialBotApiMethod;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.InlineKeyboardMarkup;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import static com.whiskels.telegram.util.TelegramUtil.createInlineKeyboardButton;
import static com.whiskels.telegram.util.TelegramUtil.createMessageTemplate;

@Slf4j
@Component
public class QuizHandler implements Handler {
    //Храним поддерживаемые CallBackQuery в виде констант
    public static final String QUIZ_CORRECT = "/quiz_correct";
    public static final String QUIZ_INCORRECT = "/quiz_incorrect";
    public static final String QUIZ_START = "/quiz_start";
    //Храним варианты ответа
    private static final List<string> OPTIONS = List.of("A", "B", "C", "D");

    private final JpaUserRepository userRepository;
    private final JpaQuestionRepository questionRepository;

    public QuizHandler(JpaUserRepository userRepository, JpaQuestionRepository questionRepository) {
        this.userRepository = userRepository;
        this.questionRepository = questionRepository;
    }

    @Override
    public List<partialbotapimethod<? extends="" serializable="">> handle(User user, String message) {
        if (message.startsWith(QUIZ_CORRECT)) {
            // действие на коллбек с правильным ответом
            return correctAnswer(user, message);
        } else if (message.startsWith(QUIZ_INCORRECT)) {
            // действие на коллбек с неправильным ответом
            return incorrectAnswer(user);
        } else {
            return startNewQuiz(user);
        }
    }

    private List<partialbotapimethod<? extends="" serializable="">> correctAnswer(User user, String message) {
        log.info("correct");
        final int currentScore = user.getScore() + 1;
        user.setScore(currentScore);
        userRepository.save(user);

        return nextQuestion(user);
    }

    private List<partialbotapimethod<? extends="" serializable="">> incorrectAnswer(User user) {
        final int currentScore = user.getScore();
        // Обновляем лучший итог
        if (user.getHighScore() < currentScore) {
            user.setHighScore(currentScore);
        }
        // Меняем статус пользователя
        user.setScore(0);
        user.setBotState(State.NONE);
        userRepository.save(user);

        // Создаем кнопку для повторного начала игры
        InlineKeyboardMarkup inlineKeyboardMarkup = new InlineKeyboardMarkup();

        List<inlinekeyboardbutton> inlineKeyboardButtonsRowOne = List.of(
                createInlineKeyboardButton("Try again?", QUIZ_START));

        inlineKeyboardMarkup.setKeyboard(List.of(inlineKeyboardButtonsRowOne));

        return List.of(createMessageTemplate(user)
                .setText(String.format("Incorrect!%nYou scored *%d* points!", currentScore))
                .setReplyMarkup(inlineKeyboardMarkup));
    }

    private List<partialbotapimethod<? extends="" serializable="">> startNewQuiz(User user) {
        user.setBotState(State.PLAYING_QUIZ);
        userRepository.save(user);

        return nextQuestion(user);
    }

    private List<partialbotapimethod<? extends="" serializable="">> nextQuestion(User user) {
        Question question = questionRepository.getRandomQuestion();

        // Собираем список возможных вариантов ответа
        List<string> options = new ArrayList<>(List.of(question.getCorrectAnswer(), question.getOptionOne(), question.getOptionTwo(), question.getOptionThree()));
        // Перемешиваем
        Collections.shuffle(options);

        // Начинаем формировать сообщение с вопроса
        StringBuilder sb = new StringBuilder();
        sb.append('*')
                .append(question.getQuestion())
                .append("*\n\n");

        InlineKeyboardMarkup inlineKeyboardMarkup = new InlineKeyboardMarkup();

        // Создаем два ряда кнопок
        List<inlinekeyboardbutton> inlineKeyboardButtonsRowOne = new ArrayList<>();
        List<inlinekeyboardbutton> inlineKeyboardButtonsRowTwo = new ArrayList<>();

        // Формируем сообщение и записываем CallBackData на кнопки
        for (int i = 0; i < options.size(); i++) {
            InlineKeyboardButton button = new InlineKeyboardButton();

            final String callbackData = options.get(i).equalsIgnoreCase(question.getCorrectAnswer()) ? QUIZ_CORRECT : QUIZ_INCORRECT;

            button.setText(OPTIONS.get(i))
                    .setCallbackData(String.format("%s %d", callbackData, question.getId()));

            if (i < 2) {
                inlineKeyboardButtonsRowOne.add(button);
            } else {
                inlineKeyboardButtonsRowTwo.add(button);
            }
            sb.append(OPTIONS.get(i) + ". " + options.get(i));
            sb.append("\n");
        }

        inlineKeyboardMarkup.setKeyboard(List.of(inlineKeyboardButtonsRowOne, inlineKeyboardButtonsRowTwo));
        return List.of(createMessageTemplate(user)
                .setText(sb.toString())
                .setReplyMarkup(inlineKeyboardMarkup));
    }

    @Override
    public State operatedBotState() {
        return null;
    }

    @Override
    public List<string> operatedCallBackQuery() {
        return List.of(QUIZ_START, QUIZ_CORRECT, QUIZ_INCORRECT);
    }
}
<h3>Заполняем базу данных</h3>Здесь мы запишем наши вопросы по аналогии со скриптом инициализации БД.
DELETE
FROM java_quiz;

INSERT INTO java_quiz (question, answer_correct, option1, option2, option3)
VALUES ('What is a correct syntax to output "Hello World" in Java?', 'System.out.println("Hello World!");',
        'print("Hello World!");', 'sout("Hello World!");', 'Systemout.print("Hello world!");'),
       ('What is the correct way to create an object called foo of Bar class?', 'Bar foo = new Bar();',
        'Foo bar = new Foo();', 'Bar foo() = new Foo();', 'Foo bar() = new Bar();'),
       ('Which operator can be used to compare two values?', '==', '=', '&', '==='),
       ('Which method can be used to return a string in upper case letters?', 'toUpperCase()', 'camelCase()',
        'upperCase()', 'formatUpper()'),
       ('Which method can be used to find the length of a string?', 'length()', 'getSize()', 'len()', 'getLength()'),
       ('Which data type is used to create a variable that should store text?', 'String', 'Text', 'Varchar', 'const'),
       ('How to insert a comment?', '// like this', '# like this', '<-- like this -->', '/ like this');
Осталось только поправить application.yaml:
bot:
  name: JavaQuiz
  token: 1234567:AAF0Wru1Z60p8vPtKihx3odbwSv9O0y_-MM
spring:
  datasource:
    url: jdbc:postgresql://ec2-54-75-199-252.eu-west-1.compute.amazonaws.com:5432/d5p9skg6nin3mh?user=bozuqwnhjhoubl&password=8a40050de8a0014c14df49aeaac5880f1ac633cc20c78a4cc3b32323231
    driver-class-name: org.postgresql.Driver
    initialization-mode: never
  jpa:
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
    hibernate:
      ddl-auto: update
URL базы данных отличается от того, что указан в поле URI на Heroku, так что вот краткая шпаргалка о том, как его составить:
jdbc:postgresql://{Host}:{Port}/{Database}?user={User}&password={Password}
Если мы все сделали правильно, то запуская main не увидим стектрейса, даже наоборот:
.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.2.4.RELEASE)

2020-10-02 19:27:43.081  INFO 14536 --- [           main] com.whiskels.telegram.App                : Starting App on Kuzmin with PID 14536 (D:\utilities\forJavaRush\target\classes started by whiskels in D:\utilities\forJavaRush)
2020-10-02 19:27:43.087  INFO 14536 --- [           main] com.whiskels.telegram.App                : No active profile set, falling back to default profiles: default
2020-10-02 19:27:44.014  INFO 14536 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2020-10-02 19:27:44.131  INFO 14536 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 102ms. Found 2 JPA repository interfaces.
2020-10-02 19:27:44.774  INFO 14536 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2020-10-02 19:27:44.885  INFO 14536 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.4.10.Final}
2020-10-02 19:27:45.053  INFO 14536 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
2020-10-02 19:27:45.278  INFO 14536 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2020-10-02 19:27:46.770  INFO 14536 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2020-10-02 19:27:46.790  INFO 14536 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.PostgreSQLDialect
2020-10-02 19:27:49.731  INFO 14536 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-10-02 19:27:49.741  INFO 14536 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-10-02 19:27:50.817  INFO 14536 --- [           main] c.g.x.bots.TelegramBotAutoConfiguration  : Starting auto config for telegram bots
2020-10-02 19:27:50.830  INFO 14536 --- [           main] c.g.x.bots.TelegramBotAutoConfiguration  : Initializing API without webhook support
2020-10-02 19:27:50.831  INFO 14536 --- [           main] c.g.x.bots.TelegramBotAutoConfiguration  : Registering polling bot: JavaQuiz
2020-10-02 19:27:51.556  INFO 14536 --- [           main] com.whiskels.telegram.App                : Started App in 9.196 seconds (JVM running for 10.023)
Попробуем пообщаться с нашим ботом: ЗнакомимсяСоздаем телеграм-бота с использованием Spring Boot Pt.3: Quiz Bot - 1ИграемСоздаем телеграм-бота с использованием Spring Boot Pt.3: Quiz Bot - 2Просим помощиСоздаем телеграм-бота с использованием Spring Boot Pt.3: Quiz Bot - 3Готово! Мы написали квиз-бота. Теперь нам предстоит огромная работа — нужно:
  • разобраться в написанном сегодня коде;
  • почитать документацию Spring Data;
  • добавить логгер;
  • заняться рефакторингом;
  • применить паттерны проектирования;
  • сделать возможным просить помощь не только при State.NONE;
  • добавить новый функционал;
  • списки лидеров;
  • выдавать только уникальные вопросы;
  • категории вопросов.
Надеюсь, эта двойная статья была вам полезна. Если так — ставьте звездочку в моем репозитории, мне будет приятно! При возникновении вопросов — обсудим их в комментариях. И напоследок — схема проекта:Создаем телеграм-бота с использованием Spring Boot Pt.3: Quiz Bot - 4 UPDATE 12.10: Код проекта выложил на гит as is без изменений
Комментарии (6)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Вячеслав Чернышов Backend Developer в Дом.рф
21 января 2021, 17:44
Скачал проект из гитлаба. От комментариев в глазах рябит. Хороший код не должен содержать комментарии (вообще). Код должен легко читаться без объяснений, что он делает.
Whiskels
Уровень 41
22 января 2021, 15:21
Спасибо, мне показалось, что учебный проект должен содержать комментарии, если он написан для тех, кто не имеет опыта работы с базами, спрингом и апи телеграма (а у пользователей javarush такого опыта и нет) Если комментарий относится не к квиз боту, а моему основному боту на гитхабе (все же в нем документации больше), то я бы разделил комментарии и джавадоки - для абстрактных и дефолтных методов добавлена документация, чтобы при имплементации их классов можно было видеть удобные всплывающие подсказки с нюансами работы. Комментарии с ссылками на stackoverflow крайне полезны, если в коде присутствует неочевидное решение, которое взято с конкретного обсуждения
Александр Плохой Senior Java Developer в freelance
3 октября 2020, 02:16
class QuizHandler требует серьезного рефакторинга
Whiskels
Уровень 41
3 октября 2020, 05:45
Полностью согласен! Мне хотелось поскорее подготовить MVP, поэтому весь класс представляет собой черновик того, как можно реализовать механизм создания квиза. Здесь, как минимум, хочется уменьшить количество повторяющегося кода путем создания класса-строителя сообщений.
Александр Плохой Senior Java Developer в freelance
3 октября 2020, 06:02
ну, стоило бы еще вынести константы в проперти, сделать имена переменных менее общими(user, message - это не комильфо), создание инстансов отдать на откуп фреймворку(иначе какой смысл тут вообще тогда Spring использовать)), заменить условные операторы и циклы полиморфизмом и подключаемыми реализациями, а по-хорошему вынести логику разных методов по разным классам (все-таки написание бота - задача практически бесконечно маштабируемая, поэтому неплохо бы создать для этого фундамент)
Whiskels
Уровень 41
3 октября 2020, 07:58
Спасибо, очень хорошие комментарии :)