0
Обложка: Telegram-бот счётчик сообщений на Java и Spring Boot

Telegram-бот счётчик сообщений на Java и Spring Boot

В этой статье я покажу, как написать Telegram-бот на Java с использованием Spring Boot, PostgreSQL и JPA. Также создадим исполняемый jar-файл. Сам же бот будет подсчитывать сообщения от пользователей и записывать эти данные в БД.

  1. Создаём Spring проект на Java
  2. Реализация базового функционала
  3. Добавление кнопок
  4. Подключение Telegram-бота на Java к базе данных
  5. Создание исполняемого jar-файла в Intellij IDEA
  6. Выводы

Создаём Spring проект на Java

Для этого воспользуемся сервисом быстрого создания Spring Initializr: он предоставляет интерфейс для генерации заготовки проекта с добавлением стандартных зависимостей. При необходимости в дальнейшем их можно настроить под свои нужды.

Мои настройки Spring Initializr выглядят так:

Обратите внимание на кнопку Add Dependencies: с её помощью можно добавить важные зависимости уже на старте.

После того, как вы всё указали, нажмите Generate, разархивируйте стартовый проект и откройте его с помощью удобной IDE. У меня это IntelliJ IDEA.

Реализация базового функционала

Для начала напишем на Java самый примитивный Telegram bot, который будет отвечать на наши сообщения.

Создание Telegram-бота и конфигурация

Начнём с того, что это Maven-проект. Сразу добавим в pom.xml дополнительные зависимости для работы с Телеграм ботом и базами данных:

В каталоге resources создадим файл config.properties, где будут храниться данные для подключения к боту и в будущем к БД.

Примечание Данный файл не следует включать в коммиты.

Теперь создадим бота. Для этого перейдём в Telegram в BotFather и создадим нового бота командой /newbot. Выбираем для него название, которое будет отображаться для всех, и его username. После этого BotFather выдаст токен для взаимодействия с бэкендом Телеграмма.

Теперь запишем в файл config.properties следующее:

bot.name = юзернейм_вашего_бота
bot.token = токен_вашего_бота
bot.chatId = id_нужного_чата

Добавим в основной каталог проекта пакет config, а внутри него создадим новый класс BotConfig.

Вы наверняка заметили, что мы добавили в pom.xml Lombok. Это популярная библиотека для сокращения кода и расширения функциональности Java. С ней и Spring наш класс BotConfig будет выглядеть очень лаконично:

@Configuration
@Data
@PropertySource("config.properties")
public class BotConfig {
    @Value("${bot.name}") String botName;
    @Value("${bot.token}") String token;
    @Value("${bot.chatId}") String chatId;
}

Что здесь происходит?

  1. @Configuration указывает, что класс содержит методы определения @Bean (наши @Value).
  2. @Data на этапе компиляции генерирует для всех полей геттеры, сеттеры, toString и предопределяет equals и hashCode.

С остальным, думаю, всё понятно.

Класс Телеграм бота на Java

Давайте теперь выйдем из пакета config и создадим в основном пакете проекта класс бота. Поскольку это бот-счётчик, назовём его CounterTelegramBot.

Сразу унаследуемся от TelegramLongPollingBot — класса, который позволяет взаимодействовать с Telegram. И имплементируем методы getBotUsernamegetBotToken и onUpdateReceived. Создадим конструктор и добавим две аннотации перед классом: @Component (авто-создание экземпляра) и @Slf4j (для работы с логером).

На старте получаем следующий класс:

@Slf4j
@Component
public class CounterTelegramBot extends TelegramLongPollingBot {
    final BotConfig config;
    
    public CounterTelegramBot(BotConfig config) { this.config = config; }
    @Override
    public String getBotUsername() { return config.getBotName(); }
    @Override
    public String getBotToken() { return config.getToken(); }
    @Override
    public void onUpdateReceived(@NotNull Update update) {}
}

Для начала сделаем так, чтобы на команду /start Telegram-бот что-то нам отвечал и выводил в логи сообщение об успехе. Другие сообщения будут выводить в логи "Unexpected message":

    @Override
    public void onUpdateReceived(@NotNull Update update) {
        if(update.hasMessage() && update.getMessage().hasText()){
            String messageText = update.getMessage().getText();
            long chatId = update.getMessage().getChatId();
            String memberName = update.getMessage().getFrom().getFirstName();

            switch (messageText){
                case "/start":
                    startBot(chatId, memberName);
                    break;
                default: log.info("Unexpected message");
            }
        }
    }

    private void startBot(long chatId, String userName) {
        SendMessage message = new SendMessage();
        message.setChatId(chatId);
        message.setText("Hello, " + userName + "! I'm a Telegram bot.");

        try {
            execute(message);
            log.info("Reply sent");
        } catch (TelegramApiException e){
            log.error(e.getMessage());
        }
    }

И последним штрихом является инициализация бота. Добавим в пакет config класс Initializer:

@Slf4j
@Component
public class Initializer {
    @Autowired CounterTelegramBot bot;

    @EventListener({ContextRefreshedEvent.class})
    public void init() {
        try {
            TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class);
            telegramBotsApi.registerBot((LongPollingBot) bot);
        } catch (TelegramApiException e) {
            log.error(e.getMessage());
        }
    }
  1. @Autowired обеспечивает контроль над тем, где и как осуществить автосвязывание (чтобы Spring автоматически подключил бота).
  2. @EventListener — слушатель, который вешаем на изменение класса.

Запустите и проверьте работу бота.

Примечание Если возникает ошибка Failed to configure a DataSource, измените аннотацию в исполняемом классе на @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}). Ошибка исчезнет, как только мы добавим информацию для доступа к БД в config.properties.

Добавление кнопок

Чтобы Telegram bot на Java и Spring Boot выглядел по-настоящему серьёзным, давайте добавим ему команду /help и пару кнопок.

Создадим в основной директории проекта пакет components. В него добавим:

1. Интерфейс BotCommands:

public interface BotCommands {
    List<BotCommand> LIST_OF_COMMANDS = List.of(
            new BotCommand("/start", "start bot"),
            new BotCommand("/help", "bot info")
    );

    String HELP_TEXT = "This bot will help to count the number of messages in the chat. " +
            "The following commands are available to you:\n\n" +
            "/start - start the bot\n" +
            "/help - help menu";
}

2. Класс Buttons:

public class Buttons {
    private static final InlineKeyboardButton START_BUTTON = new InlineKeyboardButton("Start");
    private static final InlineKeyboardButton HELP_BUTTON = new InlineKeyboardButton("Help");

    public static InlineKeyboardMarkup inlineMarkup() {
        START_BUTTON.setCallbackData("/start");
        HELP_BUTTON.setCallbackData("/help");

        List<InlineKeyboardButton> rowInline = List.of(START_BUTTON, HELP_BUTTON);
        List<List<InlineKeyboardButton>> rowsInLine = List.of(rowInline);

        InlineKeyboardMarkup markupInline = new InlineKeyboardMarkup();
        markupInline.setKeyboard(rowsInLine);

        return markupInline;
    }
}

В классе мы создаём две кнопки, которые будут расположены в одной линии. Одна из них отвечает за команду старта, а вторая — за вызов меню помощи.

Теперь немного улучшим класс CounterTelegramBot:

@Slf4j
@Component
public class CounterTelegramBot extends TelegramLongPollingBot implements BotCommands {
    final BotConfig config;
    
    public CounterTelegramBot(BotConfig config) {
        this.config = config;
        try {
            this.execute(new SetMyCommands(LIST_OF_COMMANDS, new BotCommandScopeDefault(), null));
        } catch (TelegramApiException e){
            log.error(e.getMessage());
        }
    }

    @Override
    public String getBotUsername() {
        return config.getBotName();
    }

    @Override
    public String getBotToken() {
        return config.getToken();
    }

    @Override
    public void onUpdateReceived(@NotNull Update update) {
        long chatId = 0;
        long userId = 0; //это нам понадобится позже
        String userName = null;
        String receivedMessage;

        //если получено сообщение текстом
        if(update.hasMessage()) {
            chatId = update.getMessage().getChatId();
            userId = update.getMessage().getFrom().getId();
            userName = update.getMessage().getFrom().getFirstName();

            if (update.getMessage().hasText()) {
                receivedMessage = update.getMessage().getText();
                botAnswerUtils(receivedMessage, chatId, userName);
            }
            
        //если нажата одна из кнопок бота    
        } else if (update.hasCallbackQuery()) {
            chatId = update.getCallbackQuery().getMessage().getChatId();
            userId = update.getCallbackQuery().getFrom().getId();
            userName = update.getCallbackQuery().getFrom().getFirstName();
            receivedMessage = update.getCallbackQuery().getData();

            botAnswerUtils(receivedMessage, chatId, userName);
        }
    }

    private void botAnswerUtils(String receivedMessage, long chatId, String userName) {
        switch (receivedMessage){
            case "/start":
                startBot(chatId, userName);
                break;
            case "/help":
                sendHelpText(chatId, HELP_TEXT);
                break;
            default: break;
        }
    }

    private void startBot(long chatId, String userName) {
        SendMessage message = new SendMessage();
        message.setChatId(chatId);
        message.setText("Hi, " + userName + "! I'm a Telegram bot.'");
        message.setReplyMarkup(Buttons.inlineMarkup());

        try {
            execute(message);
            log.info("Reply sent");
        } catch (TelegramApiException e){
            log.error(e.getMessage());
        }
    }

    private void sendHelpText(long chatId, String textToSend){
        SendMessage message = new SendMessage();
        message.setChatId(chatId);
        message.setText(textToSend);

        try {
            execute(message);
            log.info("Reply sent");
        } catch (TelegramApiException e){
            log.error(e.getMessage());
        }
    }
}

switch вынесли в отдельный метод, добавили обработку команд, в том числе и нажатие кнопок.

Подключение Telegram-бота на Java к базе данных

Перед началом работы установите PostgerSQL, если СУБД ещё не установлена. В случае, если вы работаете с другими СУБД, просто измените настройки доступа в файле config.properties. Для тех же, кто работает с PostgerSQL, config.properties будет выглядеть примерно так:

bot.name = юзернейм_вашего_бота
bot.token = токен_вашего_бота
bot.chatId = id_нужного_чата

#db related settings

spring.jpa.database = PostgreSQL
spring.jpa.show-sql = false
# для автоматического создания/обновления таблицы в бд
spring.jpa.hibernate.ddl-auto = update

spring.datasource.driverClassName = org.postgresql.Driver
# ниже прописываете порт и название бд
spring.datasource.url = jdbc:postgresql://localhost:5432/tg
# ваши кредлы для доступа к бд
spring.datasource.username = postgres
spring.datasource.password = root

В директорию проекта добавляем пакет database. В нём следует создать:

1. Класс User:

@Data
@Entity(name = "tg_data") //привязываемся к существующей таблице с готовыми колонками
public class User {

    @Id
    private long id; //BigInt
    private String name; //Text
    private int msg_numb; //Integer
}

2. Интерфейс UserRepository:

public interface UserRepository extends CrudRepository<User, Long> {
    @Transactional
    @Modifying
    @Query("update tg_data t set t.msg_numb = t.msg_numb + 1 where t.id is not null and t.id = :id")
    void updateMsgNumberByUserId(@Param("id") long id);
}

Данный интерфейс нам нужен для удобной работы с CrudRepository — интерфейсом данных Spring для общих операций CRUD. Сюда же вшиваем запрос на апдейт нашей таблицы: добавление +1 сообщения пользователю в случае, если он написал в чат.

В классе CounterTelegramBot объявим новый интерфейс с аннотацией @Autowired, которая говорит Spring, что в это поле нужно инжектнуть бин:

@Autowired
private UserRepository userRepository;

Там же создаём метод добавления пользователя в базу данных, если он написал впервые, и просто обновление столбца сообщений, если пользователь уже существует:

    private void updateDB(long userId, String userName) {
        if(userRepository.findById(userId).isEmpty()){
            User user = new User();
            user.setId(userId);
            user.setName(userName);
            //сразу добавляем в столбец каунтера 1 сообщение
            user.setMsg_numb(1);

            userRepository.save(user);
            log.info("Added to DB: " + user);
        } else {
            userRepository.updateMsgNumberByUserId(userId);
        }
    }

Финально обновим метод onUpdateReceived в классе CounterTelegramBot:

    @Override
    public void onUpdateReceived(@NotNull Update update) {
        long chatId = 0;
        long userId = 0;
        String userName = null;
        String receivedMessage;

        if(update.hasMessage()) {
            chatId = update.getMessage().getChatId();
            userId = update.getMessage().getFrom().getId();
            userName = update.getMessage().getFrom().getFirstName();

            if (update.getMessage().hasText()) {
                receivedMessage = update.getMessage().getText();
                botAnswerUtils(receivedMessage, chatId, userName);
            }
        } else if (update.hasCallbackQuery()) {
            chatId = update.getCallbackQuery().getMessage().getChatId();
            userId = update.getCallbackQuery().getFrom().getId();
            userName = update.getCallbackQuery().getFrom().getFirstName();
            receivedMessage = update.getCallbackQuery().getData();

            botAnswerUtils(receivedMessage, chatId, userName);
        }

        if(chatId == Long.valueOf(config.getChatId())){
            updateDB(userId, userName);
        }
    }

Примечание Вы можете не делать ограничение по chatId, но тогда следует дополнительно прописать логику для создания отдельной таблицы под каждый чат. В моём случае бот писался под конкретный чат.

Важно Не забудьте предоставить боту права администратора чата.

Создание исполняемого jar-файла в Intellij IDEA

У Telegram API есть одно неприятное ограничение, в соответствии с которым наш бот на Java позволяет достучаться только до сообщений, отправленных за последние 24 часа. Всё, что было отправлено раньше, не учтётся.

Поэтому после вы можете либо создать exe-файл с установкой времени выполнения, либо воспользоваться удалённым сервером. Например, в статье о Telegram-боте на Python мы рассказали, как настроить Docker и задеплоить бота на AWS.

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

Инструкция по созданию jar-файла:

  1. File — Project Structure — Project Settings — Artifacts — Кликаем по кнопке + — Jar — From modules with dependencies.
    Создание jar-файла в Intellij IDEA
  2. Выбираем главный класс проекта и жмем ОK.
    jar файл в Intellij IDEA
  3. После этого собираем Jar файл: Build — Build Artifact.
  4. Это создаст .jar, который при двойном клике запустит JVM, если она установлена в ОС.

На первом же скрине вы можете посмотреть структуру проекта.

Выводы

Создание Telegram-бота на Java возможно благодаря специальному классу TelegramLongPollingBot, а Spring Boot и Lombok сильно упрощают этот процесс.

Но стоит отметить, что тот же бот, написанный на Python или PHP, обойдётся вам в меньшее количество строк кода, да и туториалов по таким Телеграм-ботам значительно больше. А вот в качестве практики Java и небольшого пет-проекта, который можно представить в своём резюме, такая программа вполне подойдёт.

Остались вопросы? Задавайте их в комментариях к этой статье.