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

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

Статья из группы Random
ЧАСТЬ 1 Алоха! В предыдущей статье мы создали простого бота, который приветствовал нас на любое событие. Мы уже написали не одну тысячу строк кода, и пришла пора добавить нашему боту более сложный функционал. Сегодня мы попробуем написать простенького бота, чтобы в свободное время оттачивать свои знания Java Core перед собеседованиями (удивительно, но ни одного рабочего бота подобного рода я не нашел). Для этого мы сделаем следующее:
  • подключим внешнюю базу данных Postgres на Heroku;
  • напишем свои первые скрипты для инициализации и заполнения базы данных;
  • подключим Spring Boot Data JPA для работы с базой данных;
  • реализуем различные сценарии поведения бота.
Если это вам интересно, и дочитав статью, вы не захотите кидаться тухлыми помидорами, то ставьте звездочку в моем репозитории, мне будет приятно! При возникновении вопросов — обсудим их в комментариях. Вот, что мы получим в итоге:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 1Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 2Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 3<h2>Итак, поехали!</h2><h3>Создание базы данных на Heroku</h3>Начнем с того, что создадим свою первую внешнюю базу данных. Для локальной работы я рекомендую ознакомиться с pgAdmin. Но я хочу, чтобы вы ориентировались на то, что в будущем будете деплоить бота на Heroku, чтобы он не зависел от вашей локальной машины, а для этого давайте познакомимся с этим сервисом. Порядок действий:
  • регистрируемся на Heroku;
  • Заходим в наш dashboard -> New -> Create new app и создаем новое приложение;
  • Заходим в свежесозданное приложение, пугаемся множества кнопок, но концентрируемся на панели "Installed add-ons" - рядом с ней есть кнопка Configure Add-ons, ее мы и нажимаем;
  • В поиск вводим "Heroku Postgres", выбираем план "Hobby Dev - Free" -> Submit Order Form;
  • Открываем свежеполученную базу данных -> Settings -> View Credentials. На этой вкладке будут наши ключи для доступа к базе данных. Запоминаем их расположение - они нам потребуются, чтобы подключить базу данных, как DataSource в IDEA.
<h3>Добавляем зависимости в pom.xml</h3>В рамках работы над нашим ботом, добавим в наш pom следующие зависимости: Lombok, Spring Boot Data JPA, PostgreSQL. Стоп! Что это все такое и зачем мы это добавляем?
  • Lombok — библиотека, благодаря которой мы в разы уменьшим количество различного кода. С его помощью мы можем автоматически создавать конструкторы, сеттеры, геттеры и многое другое.
  • Spring Data JPAфреймворк для работы с базами данных (хотя это звучит слишком просто). Описание возможностей Spring Data JPA тянет на цикл статей, а указанная нами зависимость еще тянет с собой Hibernate и многое другое, так что опустим подробности и просто попробуем сегодня что-то написать с использованием Spring JPA.
  • PostgreSQL — тянем библиотеку для получения драйвера, который будет работать с нашей базой данных.
Наш pom.xml начинает выглядеть следующим образом: Properties:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 1Dependencies:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 2Если вы не читали предыдущую статью, то учтите, что здесь мы добавляем новые зависимости, так что это не полная структура pom.xml. Не забываем подгрузить эти зависимости в наш проект (например, зайдя в окошко Maven -> Reimport all Maven projects).<h3>Подключаем базу данных в IDEA</h3>Если вы пользуютесь IDEA Community Edition, то включить вкладку DataSource можно следующим образом. После добавления плагина нам нужно сконфигурировать DataSource. Для этого сначала включим отображение плагина: View —> Tool Windows -> DB Browser. В открывшемся окне нажимаем на зеленый плюс (new connection) -> PostgreSQL. Здесь нам понадобятся credentials, которые мы уже видели на Heroku. Заполняем окно:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 3И нажимаем "Test Connection". Если все сделано правильно — появится всплывающее окошко об успешном выполнении подключения к базе данных. Сохраняем наш Data Source.<h3>Создаем таблицы в базе данных</h3>Теперь давайте создадим таблицы, с которыми мы будем работать. Сначала установим себе PostgreSQL. После установки создадим файл initDB.sql в папке src/main/resources:

DROP TABLE IF EXISTS java_quiz;
DROP TABLE IF EXISTS users;
CREATE SEQUENCE global_seq START WITH 100000;

CREATE TABLE users
(
    id         INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
    chat_id    INTEGER UNIQUE                NOT NULL,
    name       VARCHAR                       NOT NULL,
    score      INTEGER             DEFAULT 0 NOT NULL,
    high_score INTEGER             DEFAULT 0 NOT NULL,
    bot_state  VARCHAR                       NOT NULL
);

CREATE TABLE java_quiz
(
    id             INTEGER PRIMARY KEY DEFAULT nextval('global_seq'),
    question       VARCHAR NOT NULL,
    answer_correct VARCHAR NOT NULL,
    option1        VARCHAR NOT NULL,
    option2        VARCHAR NOT NULL,
    option3        VARCHAR NOT NULL
);
Что делает наш скрипт? Первые две строки стирают таблицы при наличии, чтобы выполнить их повторное создание. В третьей строке создается последовательность, которая будет использоваться для создания уникальных id записям в нашу базу данных. Далее мы создаем две таблицы: для пользователей и для вопросов. У пользователя будет уникальный id, id телеграм чата, имя, количество очков (текущее и максимальное), а также текущий статус бота. У вопросов также будет уникальный id, а также поля, отвечающие за вопрос и варианты ответа к нему. Полученный скрипт мы можем выполнить, нажав на него правой кнопкой мыши и выбрав "Execute SQL Script". Особое внимание стоит уделить пункту "Cmd-Line interface" — здесь нам потребуется свежеустановленный PostgreSQL. При конфигурировании этого поля выбираем "New Cmd-Line interface" и указываем путь к psql.exe. В итоге настройки должны выглядеть примерно так:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 4Выполняем скрипт и если мы нигде не ошиблись, то результат нашей работы будет следующим:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 8<h3>Создаем модель</h3>Теперь пора возвращаться к написанию Java кода. Для сокращения статьи я опущу описание аннотаций, использованных для написания классов, чтобы вы могли сами с ними ознакомиться. Создадим пакет model, в котором у нас будет три класса:
  • AbstractBaseEntity — класс, описывающий любой объект, у которого может быть id (этот класс — сильное упрощение того, что вы можете увидеть на стажировке):
    
    package com.whiskels.telegram.model;
    
    import lombok.Getter;
    import lombok.Setter;
    
    import javax.persistence.*;
    // Аннотация, которая говорит нам, что это суперкласс для всех Entity
    // https://vladmihalcea.com/how-to-inherit-properties-from-a-base-class-entity-using-mappedsuperclass-with-jpa-and-hibernate/
    @MappedSuperclass
    // http://stackoverflow.com/questions/594597/hibernate-annotations-which-is-better-field-or-property-access
    @Access(AccessType.FIELD)
    
    // Аннотации Lombok для автогенерации сеттеров и геттеров на все поля
    @Getter
    @Setter
    public abstract class AbstractBaseEntity {
    
    // Аннотации, описывающие механизм генерации id - разберитесь в документации каждой!
        @Id
        @SequenceGenerator(name = "global_seq", sequenceName = "global_seq", allocationSize = 1, initialValue = START_SEQ)
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "global_seq")
    //  See https://hibernate.atlassian.net/browse/HHH-3718 and https://hibernate.atlassian.net/browse/HHH-12034
    //  Proxy initialization when accessing its identifier managed now by JPA_PROXY_COMPLIANCE setting
        protected Integer id;
    
        protected AbstractBaseEntity() {
        }
    }
    
  • User:
    
    package com.whiskels.telegram.model;
    
    import com.whiskels.telegram.bot.State;
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    import org.hibernate.annotations.BatchSize;
    
    import javax.persistence.*;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotNull;
    import java.util.Set;
    
    import static javax.persistence.FetchType.EAGER;
    
    @Entity
    @Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = "chat_id", name = "users_unique_chatid_idx")})
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class User extends AbstractBaseEntity {
        @Column(name = "chat_id", unique = true, nullable = false)
        @NotNull
        private Integer chatId;
    
        @Column(name = "name", unique = true, nullable = false)
        @NotBlank
        private String name;
    
        @Column(name = "score", nullable = false)
        @NotNull
        private Integer score;
    
        @Column(name = "high_score", nullable = false)
        @NotNull
        private Integer highScore;
    
        @Column(name = "bot_state", nullable = false)
        @NotBlank
        private State botState;
    
    // Конструктор нужен для создания нового пользователя (а может и нет? :))
        public User(int chatId) {
            this.chatId = chatId;
            this.name = String.valueOf(chatId);
            this.score = 0;
            this.highScore = 0;
            this.botState = State.START;
        }
    }
    
    
  • Класс Question:
    
    package com.whiskels.telegram.model;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.Table;
    import javax.validation.constraints.NotBlank;
    
    @Entity
    @Table(name = "java_quiz")
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class Question extends AbstractBaseEntity {
        @Column(name = "question", nullable = false)
        @NotBlank
        private String question;
    
        @Column(name = "answer_correct", nullable = false)
        @NotBlank
        private String correctAnswer;
    
        @Column(name = "option2", nullable = false)
        @NotBlank
        private String optionOne;
    
        @Column(name = "option1", nullable = false)
        @NotBlank
        private String optionTwo;
    
        @Column(name = "option3", nullable = false)
        @NotBlank
        private String optionThree;
    
        @Override
        public String toString() {
            return "Question{" +
                    "question='" + question + '\'' +
                    ", correctAnswer='" + correctAnswer + '\'' +
                    ", optionOne='" + optionOne + '\'' +
                    ", optionTwo='" + optionTwo + '\'' +
                    ", optionThree='" + optionThree + '\'' +
                    '}';
        }
    }
    
<h3>Создаем репозитории</h3>Теперь напишем репозитории Spring Data Jpa. Создаем пакет repository, в котором будут два интерфейса: JpaUserRepository, JpaQuestionRepository. Они будут наследоваться от JpaRepository — интерфейса Spring Data, который позволяет нам практически творить магию. Для понимания их работы рекомендую посмотреть видео Евгения Борисова. Классы будут совсем маленькие:
  • JpaUserRepository:
    
    package com.whiskels.telegram.repository;
    
    import com.whiskels.telegram.model.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.Optional;
    
    @Repository
    @Transactional(readOnly = true)
    public interface JpaUserRepository extends JpaRepository<user, integer=""> {
    // По названию метода Spring сам поймет, что мы хотим получить пользователя по переданному chatId
        Optional<user> getByChatId(int chatId);
    }
    </user></user,>
  • JpaQuestionRepository:
    
    package com.whiskels.telegram.repository;
    
    import com.whiskels.telegram.model.Question;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.Query;
    
    @Repository
    @Transactional(readOnly = true)
    public interface JpaQuestionRepository extends JpaRepository<question, integer=""> {
    // А здесь мы написали SQL Query, которая будет выбирать 1 случайный вопрос из таблицы вопросов
        @Query(nativeQuery = true, value = "SELECT *  FROM java_quiz ORDER BY random() LIMIT 1")
        Question getRandomQuestion();
    }
    </question,>
<h3>Добавляем функционал боту</h3>В классе User у нас есть поле еще не созданного класса State, которое будет сообщать нам о том, на каком этапе работы с ботом пользователь сейчас находится. Давайте его создадим в пакете /bot:

package com.whiskels.telegram.bot;

public enum State {
    NONE,
    START,
    ENTER_NAME,
    PLAYING_QUIZ,
}

Далее мы создадим пакет bot/handler, в котором объявим интерфейс handler:

package com.whiskels.telegram.bot.handler;

import com.whiskels.telegram.bot.State;
import com.whiskels.telegram.model.User;
import org.telegram.telegrambots.meta.api.methods.PartialBotApiMethod;

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

public interface Handler {

// основной метод, который будет обрабатывать действия пользователя
    List<partialbotapimethod<? extends="" serializable="">> handle(User user, String message);
// метод, который позволяет узнать, можем ли мы обработать текущий State у пользователя
    State operatedBotState();
// метод, который позволяет узнать, какие команды CallBackQuery мы можем обработать в этом классе
    List<string> operatedCallBackQuery();
}

</string></partialbotapimethod<?>
Обработчики мы создадим чуть позже, а пока давайте делегируем обработку событий новому классу UpdateReceiver, который мы создадим в корне пакета bot: ВНИМАНИЕ! Здесь и далее будут методы, которые отображаются, как List> handle(args); на деле они выглядят так, но форматтер кода их сломал:Создаем телеграм-бота с использованием Spring Boot Pt.2: Quiz Bot - 6

package com.whiskels.telegram.bot;

import com.whiskels.telegram.bot.handler.Handler;
import com.whiskels.telegram.model.User;
import com.whiskels.telegram.repository.JpaUserRepository;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.methods.PartialBotApiMethod;
import org.telegram.telegrambots.meta.api.objects.CallbackQuery;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.api.objects.Update;

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

@Component
public class UpdateReceiver {
    // Храним доступные хендлеры в списке (подсмотрел у Miroha)
    private final List<handler> handlers;
    // Имеем доступ в базу пользователей
    private final JpaUserRepository userRepository;

    public UpdateReceiver(List<handler> handlers, JpaUserRepository userRepository) {
        this.handlers = handlers;
        this.userRepository = userRepository;
    }

    // Обрабатываем полученный Update
    public List<partialbotapimethod<? extends="" serializable="">> handle(Update update) {
        // try-catch, чтобы при несуществующей команде просто возвращать пустой список
        try {
            // Проверяем, если Update - сообщение с текстом
            if (isMessageWithText(update)) {
                // Получаем Message из Update
                final Message message = update.getMessage();
                // Получаем айди чата с пользователем
                final int chatId = message.getFrom().getId();
                
                // Просим у репозитория пользователя. Если такого пользователя нет - создаем нового и возвращаем его.
                // Как раз на случай нового пользователя мы и сделали конструктор с одним параметром в классе User
                final User user = userRepository.getByChatId(chatId)
                        .orElseGet(() -> userRepository.save(new User(chatId)));
                // Ищем нужный обработчик и возвращаем результат его работы
                return getHandlerByState(user.getBotState()).handle(user, message.getText());
                
            } else if (update.hasCallbackQuery()) {
                final CallbackQuery callbackQuery = update.getCallbackQuery();
                final int chatId = callbackQuery.getFrom().getId();
                final User user = userRepository.getByChatId(chatId)
                        .orElseGet(() -> userRepository.save(new User(chatId)));

                return getHandlerByCallBackQuery(callbackQuery.getData()).handle(user, callbackQuery.getData());
            }

            throw new UnsupportedOperationException();
        } catch (UnsupportedOperationException e) {
            return Collections.emptyList();
        }
    }

    private Handler getHandlerByState(State state) {
        return handlers.stream()
                .filter(h -> h.operatedBotState() != null)
                .filter(h -> h.operatedBotState().equals(state))
                .findAny()
                .orElseThrow(UnsupportedOperationException::new);
    }

    private Handler getHandlerByCallBackQuery(String query) {
        return handlers.stream()
                .filter(h -> h.operatedCallBackQuery().stream()
                        .anyMatch(query::startsWith))
                .findAny()
                .orElseThrow(UnsupportedOperationException::new);
    }

    private boolean isMessageWithText(Update update) {
        return !update.hasCallbackQuery() && update.hasMessage() && update.getMessage().hasText();
    }
}

</partialbotapimethod<?></handler></handler>
И делегируем ему обработку в классе Bot:

package com.whiskels.telegram.bot;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.bots.TelegramLongPollingBot;
import org.telegram.telegrambots.meta.api.methods.PartialBotApiMethod;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;

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

@Slf4j
@Component
public class Bot extends TelegramLongPollingBot {
    @Value("${bot.name}")
    @Getter
    private String botUsername;

    @Value("${bot.token}")
    @Getter
    private String botToken;

    private final UpdateReceiver updateReceiver;

    public Bot(UpdateReceiver updateReceiver) {
        this.updateReceiver = updateReceiver;
    }

    @Override
    public void onUpdateReceived(Update update) {
        List<partialbotapimethod<? extends="" serializable="">> messagesToSend = updateReceiver.handle(update);

        if (messagesToSend != null && !messagesToSend.isEmpty()) {
            messagesToSend.forEach(response -> {
                if (response instanceof SendMessage) {
                    executeWithExceptionCheck((SendMessage) response);
                }
            });
        }
    }

    public void executeWithExceptionCheck(SendMessage sendMessage) {
        try {
            execute(sendMessage);
        } catch (TelegramApiException e) {
            log.error("oops");
        }
    }
}

</partialbotapimethod<?>
Теперь наш бот делегирует обработку событий классу UpdateReceiver, но обработчиков у нас еще нет. Давайте их создадим! DISCLAIMER! Мне очень хотелось поделиться возможностями по написанию такого бота, поэтому дальнейший код (как в принципе и код UpdateReceiver) можно очень хорошо отрефакторить с применением различных паттернов. Но мы учимся и наша цель — минимально жизнеспособный бот, так что в качестве еще одного домашнего задания можно отрефакторить все, что вы увидели :) Создаем пакет util, а в нем — класс TelegramUtil:

package com.whiskels.telegram.util;

import com.whiskels.telegram.model.User;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.InlineKeyboardButton;

public class TelegramUtil {
    public static SendMessage createMessageTemplate(User user) {
        return createMessageTemplate(String.valueOf(user.getChatId()));
    }

    // Создаем шаблон SendMessage с включенным Markdown
    public static SendMessage createMessageTemplate(String chatId) {
        return new SendMessage()
                .setChatId(chatId)
                .enableMarkdown(true);
    }

    // Создаем кнопку
    public static InlineKeyboardButton createInlineKeyboardButton(String text, String command) {
        return new InlineKeyboardButton()
                .setText(text)
                .setCallbackData(command);
    }
}
Мы напишем четыре обработчика: HelpHandler, QuizHandler, RegistrationHandler, StartHandler. StartHandler:

package com.whiskels.telegram.bot.handler;

import com.whiskels.telegram.bot.State;
import com.whiskels.telegram.model.User;
import com.whiskels.telegram.repository.JpaUserRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.methods.PartialBotApiMethod;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;

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

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

@Component
public class StartHandler implements Handler {
    @Value("${bot.name}")
    private String botUsername;

    private final JpaUserRepository userRepository;

    public StartHandler(JpaUserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public List<partialbotapimethod<? extends="" serializable="">> handle(User user, String message) {
        // Приветствуем пользователя
        SendMessage welcomeMessage = createMessageTemplate(user)
                .setText(String.format(
                        "Hola! I'm *%s*%nI am here to help you learn Java", botUsername
                ));
        // Просим назваться
        SendMessage registrationMessage = createMessageTemplate(user)
                .setText("In order to start our journey tell me your name");
        // Меняем пользователю статус на - "ожидание ввода имени"
        user.setBotState(State.ENTER_NAME);
        userRepository.save(user);

        return List.of(welcomeMessage, registrationMessage);
    }

    @Override
    public State operatedBotState() {
        return State.START;
    }

    @Override
    public List<string> operatedCallBackQuery() {
        return Collections.emptyList();
    }
}

</string></partialbotapimethod<?>
RegistrationHandler:

package com.whiskels.telegram.bot.handler;

import com.whiskels.telegram.bot.State;
import com.whiskels.telegram.model.User;
import com.whiskels.telegram.repository.JpaUserRepository;
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.List;

import static com.whiskels.telegram.bot.handler.QuizHandler.QUIZ_START;
import static com.whiskels.telegram.util.TelegramUtil.createInlineKeyboardButton;
import static com.whiskels.telegram.util.TelegramUtil.createMessageTemplate;

@Component
public class RegistrationHandler implements Handler {
    //Храним поддерживаемые CallBackQuery в виде констант
    public static final String NAME_ACCEPT = "/enter_name_accept";
    public static final String NAME_CHANGE = "/enter_name";
    public static final String NAME_CHANGE_CANCEL = "/enter_name_cancel";

    private final JpaUserRepository userRepository;

    public RegistrationHandler(JpaUserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public List<partialbotapimethod<? extends="" serializable="">> handle(User user, String message) {
        // Проверяем тип полученного события
        if (message.equalsIgnoreCase(NAME_ACCEPT) || message.equalsIgnoreCase(NAME_CHANGE_CANCEL)) {
            return accept(user);
        } else if (message.equalsIgnoreCase(NAME_CHANGE)) {
            return changeName(user);
        }
        return checkName(user, message);

    }

    private List<partialbotapimethod<? extends="" serializable="">> accept(User user) {
        // Если пользователь принял имя - меняем статус и сохраняем
        user.setBotState(State.NONE);
        userRepository.save(user);

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

        List<inlinekeyboardbutton> inlineKeyboardButtonsRowOne = List.of(
                createInlineKeyboardButton("Start quiz", QUIZ_START));

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

        return List.of(createMessageTemplate(user).setText(String.format(
                "Your name is saved as: %s", user.getName()))
                .setReplyMarkup(inlineKeyboardMarkup));
    }

    private List<partialbotapimethod<? extends="" serializable="">> checkName(User user, String message) {
        // При проверке имени мы превентивно сохраняем пользователю новое имя в базе 
        // идея для рефакторинга - добавить временное хранение имени
        user.setName(message);
        userRepository.save(user);

        // Делаем кнопку для применения изменений
        InlineKeyboardMarkup inlineKeyboardMarkup = new InlineKeyboardMarkup();

        List<inlinekeyboardbutton> inlineKeyboardButtonsRowOne = List.of(
                createInlineKeyboardButton("Accept", NAME_ACCEPT));

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

        return List.of(createMessageTemplate(user)
                .setText(String.format("You have entered: %s%nIf this is correct - press the button", user.getName()))
                .setReplyMarkup(inlineKeyboardMarkup));
    }
    
    private List<partialbotapimethod<? extends="" serializable="">> changeName(User user) {
        // При запросе изменения имени мы меняем State
        user.setBotState(State.ENTER_NAME);
        userRepository.save(user);

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

        List<inlinekeyboardbutton> inlineKeyboardButtonsRowOne = List.of(
                createInlineKeyboardButton("Cancel", NAME_CHANGE_CANCEL));

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

        return List.of(createMessageTemplate(user).setText(String.format(
                "Your current name is: %s%nEnter new name or press the button to continue", user.getName()))
                .setReplyMarkup(inlineKeyboardMarkup));
    }

    @Override
    public State operatedBotState() {
        return State.ENTER_NAME;
    }

    @Override
    public List<string> operatedCallBackQuery() {
        return List.of(NAME_ACCEPT, NAME_CHANGE, NAME_CHANGE_CANCEL);
    }
}

</string></inlinekeyboardbutton></partialbotapimethod<?></inlinekeyboardbutton></partialbotapimethod<?></inlinekeyboardbutton></partialbotapimethod<?></partialbotapimethod<?>
Help Handler:

package com.whiskels.telegram.bot.handler;

import com.whiskels.telegram.bot.State;
import com.whiskels.telegram.model.User;
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.methods.PartialBotApiMethod;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
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.bot.handler.RegistrationHandler.NAME_CHANGE;
import static com.whiskels.telegram.util.TelegramUtil.createInlineKeyboardButton;
import static com.whiskels.telegram.util.TelegramUtil.createMessageTemplate;

@Component
public class HelpHandler implements Handler {

    @Override
    public List<partialbotapimethod<? extends="" serializable="">> handle(User user, String message) {
        // Создаем кнопку для смены имени
        InlineKeyboardMarkup inlineKeyboardMarkup = new InlineKeyboardMarkup();

        List<inlinekeyboardbutton> inlineKeyboardButtonsRowOne = List.of(
                createInlineKeyboardButton("Change name", NAME_CHANGE));

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

        return List.of(createMessageTemplate(user).setText(String.format("" +
                "You've asked for help %s? Here it comes!", user.getName()))
        .setReplyMarkup(inlineKeyboardMarkup));

    }

    @Override
    public State operatedBotState() {
        return State.NONE;
    }

    @Override
    public List<string> operatedCallBackQuery() {
        return Collections.emptyList();
    }
}

</string></inlinekeyboardbutton></partialbotapimethod<?>
QuizHandler (самый ужасно
Комментарии (16)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
16 ноября 2021
Отличная статья. Много полезной информации. Мне как абсолютному новичку очень интересно, хотя и многое непонятно. Есть правда вопрос: не совсем понимаю как мне формировать структуру проекта. То есть какую логику где прописывать? От всех эти Handler'ов, репозиториев и куч интерфейсов голова кружится. Как в этом разобраться?
10 ноября 2021
Большой респект тебе! У меня есть ощущения что ты знаком с topjava. Очень похож стиль кода)
Anonymous #2763389 Уровень 1
24 октября 2021
Не могу понять в чём проблема, мучаюсь часа 3 уже, почему подсвечивает запрос?
Aleksei Уровень 35 Expert
15 января 2021
Да, работа с ботом проделана отличная, но подача материала страдает. Первая статья получилась хорошей, а тут возникает впечатление, что автору стало лень толково описывать сам процесс.
lanswar Уровень 28
10 октября 2020
Добрый день. Подскажите, как я могу посмотреть исходники кода, который именно здесь приведен? в вашем репозитории довольно много изменений по сравнению с этой версией. Спасибо.
3 октября 2020
а вот это что еще за master-код: List<partialbotapimethod<? extends="" serializable="">> ?