В этой статье я покажу, как написать Telegram-бот на Java с использованием Spring Boot, PostgreSQL и JPA. Также создадим исполняемый jar-файл. Сам же бот будет подсчитывать сообщения от пользователей и записывать эти данные в БД.
- Создаём Spring проект на Java
- Реализация базового функционала
- Добавление кнопок
- Подключение Telegram-бота на Java к базе данных
- Создание исполняемого jar-файла в Intellij IDEA
- Выводы
Создаём 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;
}
Что здесь происходит?
@Configuration
указывает, что класс содержит методы определения@Bean
(наши@Value
).@Data
на этапе компиляции генерирует для всех полей геттеры, сеттеры,toString
и предопределяет equals и hashCode.
С остальным, думаю, всё понятно.
Класс Телеграм бота на Java
Давайте теперь выйдем из пакета config и создадим в основном пакете проекта класс бота. Поскольку это бот-счётчик, назовём его CounterTelegramBot.
Сразу унаследуемся от TelegramLongPollingBot
— класса, который позволяет взаимодействовать с Telegram. И имплементируем методы getBotUsername
, getBotToken
и 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());
}
}
@Autowired
обеспечивает контроль над тем, где и как осуществить автосвязывание (чтобы Spring автоматически подключил бота).@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-файла:
- File — Project Structure — Project Settings — Artifacts — Кликаем по кнопке + — Jar — From modules with dependencies.
- Выбираем главный класс проекта и жмем ОK.
- После этого собираем Jar файл: Build — Build Artifact.
- Это создаст .jar, который при двойном клике запустит JVM, если она установлена в ОС.
На первом же скрине вы можете посмотреть структуру проекта.
Выводы
Создание Telegram-бота на Java возможно благодаря специальному классу TelegramLongPollingBot
, а Spring Boot и Lombok сильно упрощают этот процесс.
Но стоит отметить, что тот же бот, написанный на Python или PHP, обойдётся вам в меньшее количество строк кода, да и туториалов по таким Телеграм-ботам значительно больше. А вот в качестве практики Java и небольшого пет-проекта, который можно представить в своём резюме, такая программа вполне подойдёт.
Остались вопросы? Задавайте их в комментариях к этой статье.