Особенности создания телеграм-бота на Java
В статье пойдет разговор о том, что такое боты, для чего они используются, как работают и чем отличаются от обычных аккаунтов. Также рассмотрим порядок создания телеграм-бота на «Джава».
Ботами (bot) и чат ботами (chat bots) называют специальные аккаунты в Телеграмм, используемые для автоматической обработки и отправки сообщений. На практике пользователи взаимодействуют с ботами посредством сообщений, которые они отправляют как через обычные, так и через групповые чаты. Бот работает по определенной логике — она контролируется с помощью HTTPS-запросов к специальному API для ботов от Телеграм.

Возможности ботов
Приведем несколько классических примеров применения ботов в Телеграмм:
- Утилиты и инструменты. Телеграм-бот может переводить тексты, отображать актуальную погоду, предупреждать о каких-либо предстоящих событиях, использоваться для проведения опросов.
- Интеграция с сервисами. Бота можно использовать для отправки комментариев либо уведомлений, управления «умным домом».
- Игры (как одно-, так и многопользовательские). Бот без проблем поиграет с вами в шахматы/шашки, проведет викторину и т. п.
- Социальные сервисы. При необходимости специальный бот найдет вам собеседника, взяв за основу для поиска ваши интересы и увлечения.
- Все остальное. Это «все остальное» ограничивается лишь вашей фантазией. На деле вы можете запрограммировать бота практически для чего угодно. Однако стоит понимать, что он все равно останется ботом, а значит, не сможет помыть посуду вместо вас.
Если резюмировать вышеперечисленное одним предложением, то бот в Телеграм умеет оставлять комментарии к записям и постам, переводить тексты, искать информацию и аудио/видеоданные, спрашивать и отвечать на вопросы, подключаться к сети, обходить блокировки роутеров и сервисов, создавать чаты для общения, транслировать презентации и т. д. — всего не перечислишь.
Как функционируют боты?
Боты — особые аккаунты, по сути, представляющие собой интерфейс к вашему сервису, работающему на удаленном сервере. Плюс в том, что для создания бота вам совершенно не обязательно изучать низкоуровневые технологии, так как все взаимодействие основано на обычном HTTPS-интерфейсе с упрощенными методами API — его называют Bot API .
В реальности вы можете создать бота в Телеграмм с помощью… бота. Для этого потребуется написать пользователю @BotFather , а потом следовать его инструкциям. После создания вы получите специальный ключ авторизации (токен). Выполнить необходимые настройки можно будет в разделе документации Bot API .

Особенности создания бота с помощью Java
Если вы не ищете легких путей, хотите прокачаться в Java и привыкли все творить своими руками, вы можете написать бота, используя язык программирования Java («Джава», «Ява»). Ниже рассмотрим один из возможных алгоритмов действий.
Пишем бот на Java
На деле написать бота для Телеграмм, используя Java, не так уже сложно. Рассмотрим пример создания бота посредством Webhook.

Общая последовательность действий будет следующей:
- Открываем «Эклипс», создаем новый Java-проект.
- Находим и загружаем базу, необходимую для создания Telegram-ботов.
- Импортируем загруженную библиотеку в проект.
- Создаем класс test.SimpleBot со следующим содержимым:
— открытие веб-браузера, переход по ссылке: https://telegram.me/botfather;
— нажатие кнопки «Send message»;
— выбор BotFather в Телеграме;
— ввод имени бота на Webhook.
Также надо будет придумать имя пользователя для вновь созданного бота. Тут главное, чтобы это имя было уникальным. После ввода имени надо будет нажать кнопку подтверждения, в результате чего появится сообщение об успешной конфигурации. Обратите внимание, что после «Use this token to access the HTTP API:» выведется ваш токен, который надо будет ввести в требуемом месте.
- Переходим в «Эклипс», запускаем бота.
- В адресной строке веб-браузера набираем https://telegram.me/имя_вашего_бота (это необходимо для тестирования работоспособности).
- Нажимаем «Send message».
- Возвращаемся в Телеграм, выбираем созданного бота.
- Кликаем «Старт».
Все, Telegram-bot Webhook, написанный на «Джава», готов. На данном этапе на любое обращение робот должен отвечать что-то в стиле «Я не знаю, что ответить на это», однако эту фразу можно поменять путем дополнения базы.
Каковы плюсы Telegram-бота на Java
Можно перечислить ряд преимуществ такой реализации:
- Простота.
- Минимум выполняемых операций.
- Минимум требуемых знаний и умений.
В сети вы можете найти целый спектр уже готовых решений в виде программного кода, поэтому написать бота на Java будет не сложно. Вот, к примеру, полезное обучающее видео , где подробно рассказывается о том, как создать погодного бота на «Джава». Преимущество именно этого решения — легкость, доступность, простота создания. Дерзайте!
- https://stelegram.ru/faq/pravila-sozdaniya-telegramm-bota-na-java;
- https://tlgrm.ru/docs/bots.
Простой Telegram-бот на Java и Spring Boot

Тема разработки собственного бота для Telegram хоть и не новая, но всегда актуальная и востребованная. Думаю каждый разработчик рано или поздно сталкивается с такой задачей, поэтому сегодня я расскажу как разработать простой Telegram-бот на Java с использованием Spring Boot.
Функционал бота, который я буду разрабатывать в этой статье, очень простой — с помощью него можно будет получить официальные курсы валют с сайта ЦБ РФ (в нашем случае это доллары и евро). После прочтения данной статьи вы сможете написать уже собственный Telegram-бот с гораздо более сложным функционалом.
Так же данный материал есть в видео-формате на моём YouTube-канале:
Основа Telegram-бота
Для начала понадобится BotFather, чтобы зарегистрировать новый бот в Telegram: https://t.me/BotFather
Выполняем команду /newbot и вводим имя будущего бота, после чего BotFather сгенерирует уникальный токен, который далее потребуется для организации взаимодействия приложения с созданным Telegram-ботом:

Далее открываем Spring Initializr и генерируем новое Spring Boot приложение:

На начальном этапе я не стал включать в проект какие-либо зависимости — всё необходимое буду добавлять постепенно, чтобы было более наглядно.
Скачиваем и открываем архив со сгенерированными исходниками, после чего добавляем первую зависимость — библиотеку TelegramBots в файл build.gradle:
implementation group: 'org.telegram', name: 'telegrambots', version: '6.5.0'
Эта библиотека серьёзно упрощает взаимодействие с Telegram API и предоставляет удобные интерфейсы, которые я и буду использовать для написания бота.
TelegramBots позволяет взаимодействовать с Telegram API в двух режимах:
- Long Polling (Длинные опросы) — необходимо наследоваться от класса org.telegram.telegrambots.bots.TelegramLongPollingBot
- Webhook (Вебхук) — необходимо наследоваться от класса org.telegram.telegrambots.bots.TelegramWebhookBot
Я предпочитаю использовать первый вариант, поэтому в своём примере буду наследоваться от абстрактного класса TelegramLongPollingBot. Но прежде чем идти дальше, я хочу остановиться на исходниках библиотеки, а конкретно на конструкторах абстрактного класса TelegramLongPollingBot. На момент написания статьи реализация TelegramLongPollingBot выглядит следующим образом (версия библиотеки 6.5.0):
public abstract class TelegramLongPollingBot extends DefaultAbsSender implements LongPollingBot < /** * If this is used getBotToken has to be overridden in order to return the bot token! * @deprecated Overwriting the getBotToken() method is deprecated. Use the constructor instead */ @Deprecated() public TelegramLongPollingBot() < this(new DefaultBotOptions()); >/** * If this is used getBotToken has to be overridden in order to return the bot token! * @deprecated Overwriting the getBotToken() method is deprecated. Use the constructor instead */ @Deprecated() public TelegramLongPollingBot(DefaultBotOptions options) < super(options); >public TelegramLongPollingBot(String botToken) < this(new DefaultBotOptions(), botToken); >public TelegramLongPollingBot(DefaultBotOptions options, String botToken) < super(options, botToken); >. . . >
Обратите особое внимание что первые два конструктора помечены как @Deprecated(), а это значит что их уже не рекомендуется использовать. Это важный момент, так как в более ранних статьях по написанию бота для Telegram очень часто используется конструктор без параметров. Так как теперь у данного конструктора имеется аннотация @Deprecated(), то я его не буду использовать и вместо него возьму конструктор, у которого в сигнатуре всего один параметр — botToken:
public TelegramLongPollingBot(String botToken)
После того, как разобрались с конструкторами, можно начинать разработку. Для начала я создам в проекте 2 новых пакета — bot и configuration. Вот так на начальном этапе у меня выглядит структура папки src:

В пакет bot я добавил новый класс ExchangeRatesBot, который как раз и наследуется от абстрактного класса TelegramLongPollingBot:
package ru.akutepov.exchangeratesbot.bot; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.telegram.telegrambots.bots.TelegramLongPollingBot; import org.telegram.telegrambots.meta.api.objects.Update; @Component public class ExchangeRatesBot extends TelegramLongPollingBot < public ExchangeRatesBot(@Value("$") String botToken) < super(botToken); >@Override public void onUpdateReceived(Update update) < >@Override public String getBotUsername() < return "kutepov_exchange_rates_bot"; >>
Тут пока всё предельно просто, разберём основные моменты:
- в конструктор передаётся переменная botToken, которая будет заполняться из параметра bot.token (чуть позже я пропишу в resources/application.properties). Конечно же сам класс ExchangeRatesBot помечен аннотацией @Component, для того чтобы был создан соответствующий бин.
- метод onUpdateReceived(Update update) вызывается всякий раз, когда пользователь отправляет в бот сообщение. В этом методе я и буду обрабатывать поступающие от пользователя команды.
- метод getBotUsername() должен возвращать название бота, которое тоже можно поместить в проперти. Но лично я в этом смысла не вижу, так как название бота не является конфиденциальной информацией в отличие от токена и его вполне можно и захардкодить. Если пихать в проперти-файл всё подряд, то можно получить антипаттерн Мягкое кодирование (Soft code), о нём я рассказывал в одном из своих видео: https://youtu.be/os7VVNsX5pI
И следующим логичным шагом будет добавление параметра bot.token в файл resources/application.properties:

Завершающим шагом подготовки «скелета» Telegram-бота будет добавление класса ExchangeRatesBotConfiguration в пакет configuration:
package ru.akutepov.exchangeratesbot.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import org.telegram.telegrambots.updatesreceivers.DefaultBotSession; import ru.akutepov.exchangeratesbot.bot.ExchangeRatesBot; @Configuration public class ExchangeRatesBotConfiguration < @Bean public TelegramBotsApi telegramBotsApi(ExchangeRatesBot exchangeRatesBot) throws TelegramApiException < var api = new TelegramBotsApi(DefaultBotSession.class); api.registerBot(exchangeRatesBot); return api; >>
Тут я создаю новый бин TelegramBotsApi и регистрирую в нём класс бота — ExchangeRatesBot. После этого можно собрать приложение и запустить, чтобы убедиться что ничего не падает.
Получение данных для бота
Прежде чем обрабатывать команды пользователя, необходимо разработать механизм получения данных, которые планируется отправлять пользователю. В моём случае это курсы доллара и евро, которые можно взять с официального сайта ЦБ РФ: http://www.cbr.ru/scripts/XML_daily.asp
Данная ссылка возвращает XML, который содержит информацию о курсах валют, установленных на текущий день. Telegram-бот должен будет уметь запрашивать данный XML с сервиса ЦБ РФ и парсить его. Для реализации такой возможности я решил использовать простые библиотеки okhttp и XPath.
И если XPath доступен из коробки, то зависимость для okhttp придётся добавить в файл build.gradle:
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.11.0'
После этого можно приступить к разработке клиента, который будет ходить в ЦБ РФ за данными.
Сперва нужно сконфигурировать бин OkHttpClient в классе ExchangeRatesBotConfiguration, чтобы иметь возможности выполнять http-запросы с помощью библиотеки okhttp:
@Bean public OkHttpClient okHttpClient()
Затем прописать в resources/application.properties новый параметр:
cbr.currency.rates.xml.url=http://www.cbr.ru/scripts/XML_daily.asp
Кроме этого, для удобства я решил создать checked-исключение, поэтому добавил в проект пакет exception и класс ServiceException в него:
package ru.akutepov.exchangeratesbot.exception; public class ServiceException extends Exception < public ServiceException(String message) < super(message); >public ServiceException(String message, Throwable cause) < super(message, cause); >>
Небольшое отступление: ситуация с checked-исключенями на самом деле неоднозначная. С одной стороны они гарантируют что исключения будут обработаны, с другой стороны негативно влияют на читаемость кода, так как логика обработки checked-исключений расползается по всему приложению. Например Sonar Qube в дефолтных настройках будет ругаться на участки кода, которые выбрасывают checked-исключения (https://rules.sonarsource.com/java/RSPEC-1162). Моё мнение такое — в отдельных ситуациях checked-исключения бывают очень полезны и прекрасно встраиваются в бизнес-логику, однако если ими злоупотреблять, то действительно можно получить очень переусложнённый код.
И наконец я добавил новый пакет client и создал в нём класс CbrClient, в котором написал следующий код:
package ru.akutepov.exchangeratesbot.client; import okhttp3.OkHttpClient; import okhttp3.Request; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import ru.akutepov.exchangeratesbot.exception.ServiceException; import java.io.IOException; import java.util.Optional; @Component public class CbrClient < @Autowired private OkHttpClient client; @Value("$") private String cbrCurrencyRatesXmlUrl; public Optional getCurrencyRatesXML() throws ServiceException < var request = new Request.Builder() .url(cbrCurrencyRatesXmlUrl) .build(); try (var response = client.newCall(request).execute()) < var body = response.body(); return body == null ? Optional.empty() : Optional.of(body.string()); >catch (IOException e) < throw new ServiceException("Ошибка получения курсов валют от ЦБ РФ", e); >> >
Класс CbrClient будет далее использоваться как отдельный бин, поэтому не забываем про аннотацию @Component, чтобы Spring смог его найти. Для того чтобы выполнить запрос к сервису ЦБ РФ, требуется url, который я достаю из пропертей с помощью аннотации @Value и записываю в переменную cbrCurrencyRatesXmlUrl и бин OkHttpClient, который получаю с помощью аннотации @Autowired.
В методе getCurrencyRatesXML() я формирую запрос в виде объекта класса Request и далее его передаю в метод newCall(request) и вызываю метод execute() для выполнения http-запроса с помощью библиотеки okhttp. Метод execute() возвращает объект класса Response, который имплиментирует интерфейс AutoClosable, поэтому я использовал конструкцию try с параметрами.
У класса Response имеется метод body(), который возвращает тело сообщения. В данном случае сервис ЦБ РФ вернёт XML с курсами валют, вот его я и вытаскиваю из тела в виде строки, если тело не пустое:
. . . 840 USD 1 Доллар США 77,2041 978 EUR 1 Евро 84,2500 . . .
Клиент для API ЦБ РФ готов и теперь нужно создать сервисный слой, в котором из XML будут вытаскиваться необходимые данные. В приложении я создал пакет service, в него положил интерфейс ExchangeRatesService и пакет impl, в котором хранится имплементация интерфейса — ExchangeRatesServiceImpl:

В интерфейсе ExchangeRatesService всего 2 метода:
package ru.akutepov.exchangeratesbot.service; import ru.akutepov.exchangeratesbot.exception.ServiceException; public interface ExchangeRatesService
Как вы можете догадаться, getUSDExchangeRate() будет возвращать курс доллара, а getEURExchangeRate() — курс евро. Поскольку курсы валют потребуются для формирования текстового сообщения, то я не заморачиваюсь и возвращаю их сразу в виде строки. А так работа с деньгами в приложениях это отдельная интересная тема 🙂
Реализация сервисного класса ExchangeRatesServiceImpl, который имплиментирует интерфейс ExchangeRatesService, выглядит так:
package ru.akutepov.exchangeratesbot.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.w3c.dom.Document; import org.xml.sax.InputSource; import ru.akutepov.exchangeratesbot.client.CbrClient; import ru.akutepov.exchangeratesbot.exception.ServiceException; import ru.akutepov.exchangeratesbot.service.ExchangeRatesService; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.StringReader; @Service public class ExchangeRatesServiceImpl implements ExchangeRatesService < private static final String USD_XPATH = "/ValCurs//Valute[@ID='R01235']/Value"; private static final String EUR_XPATH = "/ValCurs//Valute[@ID='R01239']/Value"; @Autowired private CbrClient client; @Override public String getUSDExchangeRate() throws ServiceException < var xmlOptional = client.getCurrencyRatesXML(); String xml = xmlOptional.orElseThrow( () ->new ServiceException("Не удалось получить XML") ); return extractCurrencyValueFromXML(xml, USD_XPATH); > @Override public String getEURExchangeRate() throws ServiceException < var xmlOptional = client.getCurrencyRatesXML(); String xml = xmlOptional.orElseThrow( () ->new ServiceException("Не удалось получить XML") ); return extractCurrencyValueFromXML(xml, EUR_XPATH); > private static String extractCurrencyValueFromXML(String xml, String xpathExpression) throws ServiceException < var source = new InputSource(new StringReader(xml)); try < var xpath = XPathFactory.newInstance().newXPath(); var document = (Document) xpath.evaluate("/", source, XPathConstants.NODE); return xpath.evaluate(xpathExpression, document); >catch (XPathExpressionException e) < throw new ServiceException("Не удалось распарсить XML", e); >> >
Вначале я вынес в константы выражения для XPath:
private static final String USD_XPATH = "/ValCurs//Valute[@ID='R01235']/Value"; private static final String EUR_XPATH = "/ValCurs//Valute[@ID='R01239']/Value";
По сути это правила, по которым выполняется поиск нужных данных в XML.
Далее через @Autowired я подтягиваю CbrClient с помощью которого я буду отправлять запросы к API ЦБ РФ.
Теперь перейдём в конец и рассмотрим самый интересный метод — extractCurrencyValueFromXML(String xml, String xpathExpression). Он получает на вход строку с XML и выражение для XPath, которое представляет из себя правило, по которому в XML будет осуществлён поиск нужного значения (в нашем случае это курс валюты).
Методы getUSDExchangeRate() и getEURExchangeRate() сначала получают через клиентский бин актуальный XML, вызывая метод getCurrencyRatesXML(), а затем через метод extractCurrencyValueFromXML вытаскивают курс нужной валюты, передавая в него соответствующее выражение для XPath.
Важно! При каждом вызове метода getUSDExchangeRate() или getEURExchangeRate() выполняется запрос к API ЦБ РФ и в ответ приходит один и тот же XML, данные в котором изменятся только на следующий день. В реальном проекте такой подход не приемлем и для таких случаев следует использовать кэширование. Это увеличит быстродействие приложения и снизит нагрузку на внешние сервисы.
Обработка команд
Так как возможность получения необходимых данных в приложении реализована, то можно приступить к обработке пользовательских команд. Для этого вернёмся в класс ExchangeRatesBot и допишем его. Кода будет много, но поэтапно всё разберём:
package ru.akutepov.exchangeratesbot.bot; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; 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.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import ru.akutepov.exchangeratesbot.exception.ServiceException; import ru.akutepov.exchangeratesbot.service.ExchangeRatesService; import java.time.LocalDate; @Component public class ExchangeRatesBot extends TelegramLongPollingBot < private static final Logger LOG = LoggerFactory.getLogger(ExchangeRatesBot.class); private static final String START = "/start"; private static final String USD = "/usd"; private static final String EUR = "/eur"; private static final String HELP = "/help"; @Autowired private ExchangeRatesService exchangeRatesService; public ExchangeRatesBot(@Value("$") String botToken) < super(botToken); >@Override public void onUpdateReceived(Update update) < if (!update.hasMessage() || !update.getMessage().hasText()) < return; >String message = update.getMessage().getText(); Long chatId = update.getMessage().getChatId(); switch (message) < case START -> < String userName = update.getMessage().getChat().getUserName(); startCommand(chatId, userName); >case USD -> usdCommand(chatId); case EUR -> eurCommand(chatId); case HELP -> helpCommand(chatId); default -> unknownCommand(chatId); > > @Override public String getBotUsername() < return "kutepov_exchange_rates_bot"; >private void startCommand(Long chatId, String userName) < var text = """ Добро пожаловать в бот, %s! Здесь Вы сможете узнать официальные курсы валют на сегодня, установленные ЦБ РФ. Для этого воспользуйтесь командами: /usd - курс доллара /eur - курс евро Дополнительные команды: /help - получение справки """; var formattedText = String.format(text, userName); sendMessage(chatId, formattedText); >private void usdCommand(Long chatId) < String formattedText; try < var usd = exchangeRatesService.getUSDExchangeRate(); var text = "Курс доллара на %s составляет %s рублей"; formattedText = String.format(text, LocalDate.now(), usd); >catch (ServiceException e) < LOG.error("Ошибка получения курса доллара", e); formattedText = "Не удалось получить текущий курс доллара. Попробуйте позже."; >sendMessage(chatId, formattedText); > private void eurCommand(Long chatId) < String formattedText; try < var usd = exchangeRatesService.getEURExchangeRate(); var text = "Курс евро на %s составляет %s рублей"; formattedText = String.format(text, LocalDate.now(), usd); >catch (ServiceException e) < LOG.error("Ошибка получения курса евро", e); formattedText = "Не удалось получить текущий курс евро. Попробуйте позже."; >sendMessage(chatId, formattedText); > private void helpCommand(Long chatId) < var text = """ Справочная информация по боту Для получения текущих курсов валют воспользуйтесь командами: /usd - курс доллара /eur - курс евро """; sendMessage(chatId, text); >private void unknownCommand(Long chatId) < var text = "Не удалось распознать команду!"; sendMessage(chatId, text); >private void sendMessage(Long chatId, String text) < var chatIdStr = String.valueOf(chatId); var sendMessage = new SendMessage(chatIdStr, text); try < execute(sendMessage); >catch (TelegramApiException e) < LOG.error("Ошибка отправки сообщения", e); >> >
В самом начале я добавил логгер, вынес все команды в константы и подтянул ExchangeRatesService для того чтобы была возможность получать актуальные курсы валют:
private static final Logger LOG = LoggerFactory.getLogger(ExchangeRatesBot.class); private static final String START = "/start"; private static final String USD = "/usd"; private static final String EUR = "/eur"; private static final String HELP = "/help"; @Autowired private ExchangeRatesService exchangeRatesService;
Всего будет 4 команды, которые можно будет отправить в бот:
- /start — начало работы с ботом, первичная инструкция пользователю
- /usd — получение курса доллара
- /eur — получение курса евро
- /help — справка
Далее я в самом конце добавил метод sendMessage(Long chatId, String text), который будет отправлять пользователю сообщение:
private void sendMessage(Long chatId, String text) < var chatIdStr = String.valueOf(chatId); var sendMessage = new SendMessage(chatIdStr, text); try < execute(sendMessage); >catch (TelegramApiException e) < LOG.error("Ошибка отправки сообщения", e); >>
В метод передаются 2 параметра:
- chatId — идентификатор чата с пользователем, в который необходимо отправить сообщение
- text — текст сообщения
Теперь добавляем методы-обработчики для каждой команды. Если разрабатывать серьёзный Telegram-бот, то скорее всего придётся создавать отдельные классы-обработчики для каждой команды, чтобы соответствовать Single-Responsibility Principle (SRP) из SOLID. Но так как проект учебный и обработчики у нас не сложные, то пойдём по более простому пути.
Метод-обработчик команды /start:
private void startCommand(Long chatId, String userName) < var text = """ Добро пожаловать в бот, %s! Здесь Вы сможете узнать официальные курсы валют на сегодня, установленные ЦБ РФ. Для этого воспользуйтесь командами: /usd - курс доллара /eur - курс евро Дополнительные команды: /help - получение справки """; var formattedText = String.format(text, userName); sendMessage(chatId, formattedText); >
В данном методе просто формируется сообщение пользователю и отправляется с помощью метода sendMessage(Long chatId, String text). В сигнатуре метода обработчика startCommand 2 параметра:
- chatId — идентификатор чата с пользователем, в который необходимо отправить сообщение
- userName — уникальное имя пользователя в Telegram (ник).
Метод-обработчик команды /usd:
private void usdCommand(Long chatId) < String formattedText; try < var usd = exchangeRatesService.getUSDExchangeRate(); var text = "Курс доллара на %s составляет %s рублей"; formattedText = String.format(text, LocalDate.now(), usd); >catch (ServiceException e) < LOG.error("Ошибка получения курса доллара", e); formattedText = "Не удалось получить текущий курс доллара. Попробуйте позже."; >sendMessage(chatId, formattedText); >
В этом методе пробуем получить актуальный курс доллара и отправить пользователю сообщение. В случае ошибки — пишем её в лог и честно признаёмся пользователю что что-то пошло не так. В сигнатуре метода только параметр chatId.
Метод-обработчик команды /eur:
private void eurCommand(Long chatId) < String formattedText; try < var usd = exchangeRatesService.getEURExchangeRate(); var text = "Курс евро на %s составляет %s рублей"; formattedText = String.format(text, LocalDate.now(), usd); >catch (ServiceException e) < LOG.error("Ошибка получения курса евро", e); formattedText = "Не удалось получить текущий курс евро. Попробуйте позже."; >sendMessage(chatId, formattedText); >
Метод аналогичен предыдущему, только получает актуальный курс евро, а не доллара.
Метод-обработчик команды /help:
private void helpCommand(Long chatId) < var text = """ Справочная информация по боту Для получения текущих курсов валют воспользуйтесь командами: /usd - курс доллара /eur - курс евро """; sendMessage(chatId, text); >
Просто формируем сообщение со справочной информацией о командах бота и отправляем это сообщение пользователю.
Ну и наивно полагать что пользователи не будут слать всякую фигню в бот. Поэтому сделаем обработчик и на этот случай:
private void unknownCommand(Long chatId)
Методы обработчики готовы, теперь осталось доработать метод onUpdateReceived:
@Override public void onUpdateReceived(Update update) < if (!update.hasMessage() || !update.getMessage().hasText()) < return; >String message = update.getMessage().getText(); Long chatId = update.getMessage().getChatId(); switch (message) < case START -> < String userName = update.getMessage().getChat().getUserName(); startCommand(chatId, userName); >case USD -> usdCommand(chatId); case EUR -> eurCommand(chatId); case HELP -> helpCommand(chatId); default -> unknownCommand(chatId); > >
Как вы видите, данный метод получает команды от пользователя и решает какому обработчику какую команду передать с помощью оператора выбора switch case. Команды приходят в бот в виде текстового сообщения:
String message = update.getMessage().getText();
Идентификатор чата с пользователем можно получить следующим образом:
Long chatId = update.getMessage().getChatId();
А узнать как зовут пользователя, можно так:
update.getMessage().getChat().getUserName(); // ник update.getMessage().getChat().getFirstName(); // имя update.getMessage().getChat().getLastName(); // фамилия
Вот и всё, Telegram-бот готов! Осталось только запустить приложение и радоваться жизни:
Telegram боты на Java и где они обитают
В этом посте хочется разобрать создание ботов в телеграмме, ведь их очень интересно писать (по крайней мере, для новичков).
- написать расширяемого бота
- использовать спринг
Для начала нам нужно создать приложение на спринге. Но я думаю, каждый уже умеет это делать.
Затем добавим зависимости, многие пользуются telegrambots-spring-boot-starter, но мне как-то не довелось увидеться с ним, поэтому используем самый обычный API.
org.telegram telegrambots 6.5.0
Теперь создадим файл application.yaml в папке resources. В нём напишем токен бота.
Telegram-bots ещё требует имя, но вводить настоящее — не обязательно.
bot: token: 6098243395:AAFwSeKCFxh6kOTPPfcSYTdTuhqRZyBfULA
Создадим наш первый и основной компонент. В нём мы будем регистрировать бота и обрабатывать сообщения.
@Component public class BotComponent extends TelegramLongPollingBot < // Создаём их объект для регистрации private final TelegramBotsApi telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class); // Достаём токен бота @Value("$") private String botToken; @PostConstruct private void init() throws TelegramApiException < telegramBotsApi.registerBot(this); // Регистрируем бота >public BotComponent() throws TelegramApiException <> @Override public void onUpdateReceived(Update update) < //Проверим, работает ли наш бот. System.out.println(update.getMessage().getText()); >@Override public String getBotUsername() < return "bot"; >@Override public String getBotToken() < return botToken; >>
Теперь начинаем работать с косяками api телеграмма и как-то их обрабатывать.
Самая главная проблема — у api телеграмма отсутствует один общий интерфейс, который бы объединял все возможные виды апдейта (за исключением BotApiMethod). Обычное сообщение и SendPhoto разделены и у них нет ничего общего, а нам нужно выдавить абстракции для того, чтобы всё легко расширялось, поэтому нам придётся поговнокодить. (Возможно реализация этого может выглядеть лучше).
В том числе, нам нужно определить тип сообщения, для дальнейшего правильного использования.
Для этого создадим класс ClassifiedUpdate. Я использую Lombok, если вас это испугало, то почитайте, что это такое.
public class ClassifiedUpdate < @Getter private final TelegramType telegramType; // enum, чтобы всё выглядило красиво @Getter private final Long userId; // тот же chat-id, но выглядит красивее и получить его легче @Getter private String name; // получим имя пользователя. Именно имя, не @username @Getter private String commandName; // если это команда, то запишем её @Getter private final Update update; // сохраним сам update, чтобы в случае чего, его можно было достать @Getter private final Listargs; // просто поделим текст сообщения, в будущем это поможет @Getter private String userName; // @username public ClassifiedUpdate(Update update) < this.update = update; this.telegramType = handleTelegramType(); this.userId = handleUserId(); this.args = handleArgs(); this.commandName = handleCommandName(); >//Обработаем команду. public String handleCommandName() < if(update.hasMessage()) < if(update.getMessage().hasText()) < if(update.getMessage().getText().startsWith("/")) < return update.getMessage().getText().split(" ")[0]; >else return update.getMessage().getText(); > > if(update.hasCallbackQuery()) < return update.getCallbackQuery().getData().split(" ")[0]; >return ""; > //Обработаем тип сообщения private TelegramType handleTelegramType() < if(update.hasCallbackQuery()) return TelegramType.CallBack; if(update.hasMessage()) < if(update.getMessage().hasText()) < if(update.getMessage().getText().startsWith("/")) return TelegramType.Command; else return TelegramType.Text; >else if(update.getMessage().hasSuccessfulPayment()) < return TelegramType.SuccessPayment; >else if(update.getMessage().hasPhoto()) return TelegramType.Photo; > else if(update.hasPreCheckoutQuery()) < return TelegramType.PreCheckoutQuery; >else if(update.hasChatJoinRequest()) < return TelegramType.ChatJoinRequest; >else if(update.hasChannelPost()) < return TelegramType.ChannelPost; >else if(update.hasMyChatMember()) < return TelegramType.MyChatMember; >if(update.getMessage().hasDocument()) < return TelegramType.Text; >return TelegramType.Unknown; > //Достанем userId, имя и username из любого типа сообщений. private Long handleUserId() < if (telegramType == TelegramType.PreCheckoutQuery) < name = getNameByUser(update.getPreCheckoutQuery().getFrom()); userName = update.getPreCheckoutQuery().getFrom().getUserName(); return update.getPreCheckoutQuery().getFrom().getId(); >else if(telegramType == TelegramType.ChatJoinRequest) < name = getNameByUser(update.getChatJoinRequest().getUser()); userName = update.getChatJoinRequest().getUser().getUserName(); return update.getChatJoinRequest().getUser().getId(); >else if (telegramType == TelegramType.CallBack) < name = getNameByUser(update.getCallbackQuery().getFrom()); userName = update.getCallbackQuery().getFrom().getUserName(); return update.getCallbackQuery().getFrom().getId(); >else if(telegramType == TelegramType.MyChatMember) < name = update.getMyChatMember().getChat().getTitle(); userName = update.getMyChatMember().getChat().getUserName(); return update.getMyChatMember().getFrom().getId(); >else < name = getNameByUser(update.getMessage().getFrom()); userName = update.getMessage().getFrom().getUserName(); return update.getMessage().getFrom().getId(); >> //Разделим сообщение на аргументы private List handleArgs() < Listlist = new LinkedList<>(); if(telegramType == TelegramType.Command) < String[] args = getUpdate().getMessage().getText().split(" "); Collections.addAll(list, args); list.remove(0); return list; >else if (telegramType == TelegramType.Text) < list.add(getUpdate().getMessage().getText()); return list; >else if (telegramType == TelegramType.CallBack) < String[] args = getUpdate().getCallbackQuery().getData().split(" "); Collections.addAll(list, args); list.remove(0); return list; >return new ArrayList<>(); > //Вынесли имя в другой метод private String getNameByUser(User user) < if(user.getIsBot()) return "BOT"; if(!user.getFirstName().isBlank() || !user.getFirstName().isEmpty()) return user.getFirstName(); if(!user.getUserName().isBlank() || !user.getUserName().isEmpty()) return user.getUserName(); return "noname"; >//Лог public String getLog()
Это выглядит ужасно и некрасиво, обязательно как-то отрефакторим это, но не сегодня.
Хотел бы объяснить, зачем я разделил @username и Имя Фамилия.
Дело в том, что некоторые пользователи не имеют имя и фамилию в настройках профиля, а некоторые имеют только это. В общем, мы предусмотрели этот момент. И теперь если мы захотим написать: Привет, Илья! У нас никогда не будет: Привет, null!. Мы ведь не хотим отставать от глаза бога.
Тем, кому лень писать код, держите TelegramType:
public enum TelegramType
Двигаемся дальше, мы обработали их апдейт и теперь нам пора обработать свой апдейт, но перед этим нам нужно создать ещё свой ответ. Выглядит он не так ужасно, но ужасно 🙂
Это нам очень сильно поможет в будущем, нужно только верить.
@Data public class Answer < private SendDocument sendDocument; private SendPhoto sendPhoto; private SendVideo sendVideo; private SendVideoNote sendVideoNote; private SendSticker sendSticker; private SendAudio sendAudio; private SendVoice sendVoice; private SendMediaGroup sendMediaGroup; private SetChatPhoto setChatPhoto; private AddStickerToSet addStickerToSet; private SetStickerSetThumb setStickerSetThumb; private CreateNewStickerSet createNewStickerSet; private UploadStickerFile uploadStickerFile; private EditMessageMedia editMessageMedia; private SendAnimation sendAnimation; private BotApiMethodbotApiMethod; >
На самом деле, всё можно сделать и без этого класса, если вы собираетесь отвечать пользователю только сообщениями или коллбэками. Потому что в будущем этот класс ещё и увеличит немного кода. Я лишь стараюсь увеличить расширяемость, чтобы внедрение новой фичи делалось быстро и легко.
Теперь нам как-то нужно работать с пользователями, поэтому с помощью Spring JPA создадим сущность пользователя.
@Entity @Table(name = "users") @Getter @Setter public class User
Как вы можете заметить, у пользователя есть состояние, это поможет нам для проведения интерактивов и т.д. Также я использую у permissions тип Long, потому что обычно это:
- 0 — Default User
- 1 — Какой-нибудь VIP
- 2 — Moderator
- 3 — Admin
Это просто и удобно и лениво, но если кто-то хочет, то может заморочиться.
Вернёмся к состоянию, напишем простую сущность для состояния :
@Entity @Table(name = "state") @Getter @Setter public class State < @GeneratedValue(strategy = GenerationType.IDENTITY) @Id private Long Id; @Column(name = "value") private String stateValue; public boolean inState() < return stateValue != null; >@OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL) @JoinColumn private User user; >
Для чего нам нужно состояние?
К примеру, пользователь захотел пополнить баланс, и мы просим его ввести сумму пополнения. Если мы не узнаем, что прямо сейчас он вводит сумму пополнения, то будем обрабатывать его команду: 100, как обычную. В общем, нам нужно состояние.
Дальше нам нужно создать обработчик сообщений, в нашем случае они будут разные и их будет много, поэтому создадим интерфейс Handler.
@MappedSuperclass public interface Handler < // Какой тип сообщения будет обработан TelegramType getHandleType(); // Приоритет обработчика int priority(); // Условия, при которых мы воспользуемся этим обработчиком boolean condition(User user, ClassifiedUpdate update); // В этом методе, с помощью апдейта мы будем получать answer Answer getAnswer(User user, ClassifiedUpdate update); >
Обработчик выполняет функцию хранения комманд. Теперь нам нужно создать команды для обработчика. Создадим интерфейс Command.
@MappedSuperclass public interface Command < // Каким обработчиком будет пользоваться команда Class handler(); // С помощью чего мы найдём эту команду Object getFindBy(); // Ну и тут мы уже получим ответ на самом деле Answer getAnswer(ClassifiedUpdate update, User user); >
Теперь как-то надо найти команды для обработчика, поэтому создадим класс AbstractHandler.
@MappedSuperclass public abstract class AbstractHandler implements Handler < protected final MapallCommands = new HashMap<>(); // Найдём все команды для обработчика @Autowired private List commands; protected abstract HashMap createMap(); // Тут мы распихиваем команды по хэшмапе, чтобы потом было удобнее доставать :/ @PostConstruct private void init() < commands.forEach(c -> < allCommands.put(c.getFindBy(), c); if(Objects.equals(c.handler().getName(), this.getClass().getName())) < createMap().put(c.getFindBy(), c); System.out.println(c.getClass().getSimpleName() + " was added for " + this.getClass().getSimpleName()); >>); > >
Это конечно всё хорошо, но нам нужно собрать все обработчики в одном месте. И отправить наш ClassifiedUpdate в эту бездонную бочку. Назовём эту штуку HandlersMap, просто потому что я снова распихиваю обработчики по хэшмапе 🙂
@Component public class HandlersMap < private HashMap> hashMap = new HashMap<>(); private final List handlers; // Тут точно также находим все обработчики, просто в первом случае я использовал // @Autowired. Это немного лучше. public HandlersMap(List handlers) < this.handlers = handlers; >@PostConstruct private void init() < for(Handler handler : handlers) < if(!hashMap.containsKey(handler.getHandleType())) hashMap.put(handler.getHandleType(), new ArrayList<>()); hashMap.get(handler.getHandleType()).add(handler); > hashMap.values().forEach(h -> h.sort(new Comparator() < @Override public int compare(Handler o1, Handler o2) < return o2.priority() - o1.priority(); >>)); > public Answer execute(ClassifiedUpdate classifiedUpdate, User user) < if(!hashMap.containsKey(classifiedUpdate.getTelegramType())) return new Answer(); for (Handler handler : hashMap.get(classifiedUpdate.getTelegramType())) < if(handler.condition(user, classifiedUpdate)) return handler.getAnswer(user, classifiedUpdate); >return null; > >
Теперь нам нужна ещё прослойка в виде ClassifiedUpdateHandler’a. Там мы будем доставать пользователя из базы данных и может что-то ещё. Просто добавим его.
Класс ClassifiedUpdateHandler:
@Service public class ClassifiedUpdateHandler < private final UserService userService; private final HandlersMap commandMap; public ClassifiedUpdateHandler(UserService userService, HandlersMap commandMap) < this.userService = userService; this.commandMap = commandMap; >public Answer request(ClassifiedUpdate classifiedUpdate) < return commandMap.execute(classifiedUpdate, userService.findUserByUpdate(classifiedUpdate)); >>
Тут ничего особенного, пропустим объяснения. Намного интереснее в классе UserService.
До этого, благо, мы успели всё обработать и на 100% достать id пользователя и его имя.
@Service public class UserService < private final UserRepository userRepository; private final StateRepository stateRepository; public UserService(UserRepository userRepository, StateRepository stateRepository) < this.userRepository = userRepository; this.stateRepository = stateRepository; >public User findUserByUpdate(ClassifiedUpdate classifiedUpdate) < // Проверим, существует ли этот пользователь. if(userRepository.findByChatId(classifiedUpdate.getUserId()) != null) < User user = userRepository.findByChatId(classifiedUpdate.getUserId()); // Если мы не смогли до этого записать имя пользователя, то запишем его. if(user.getUserName() == null && classifiedUpdate.getUserName() != null) user.setUserName(classifiedUpdate.getUserName()); // Проверим менял ли пользователя имя. if(user.getUserName() != null) if (!user.getUserName().equals(classifiedUpdate.getUserName())) user.setUserName(classifiedUpdate.getUserName()); if(!user.getName().equals(classifiedUpdate.getName())) user.setName(classifiedUpdate.getName()); return user; >try < User user = new User(); user.setName(classifiedUpdate.getName()); user.setPermissions(0L); user.setChatId(classifiedUpdate.getUserId()); user.setUserName(classifiedUpdate.getUserName()); State state = new State(); state.setStateValue(null); state.setUser(user); stateRepository.save(state); user.setState(state); userRepository.save(user); return user; >catch (Exception e) < e.printStackTrace(); >return null; > >
Всё готово, теперь пора создать наш первый Handler и Command для примера. Но для начала напишем Builder для сообщений.
public class SendMessageBuilder < private SendMessage sendMessage; public SendMessageBuilder() < this.sendMessage = new SendMessage(); >public SendMessageBuilder chatId(Long chatId) < this.sendMessage.setChatId(chatId); return this; >public SendMessageBuilder message(String message) < this.sendMessage.setText(message); return this; >public Answer build() throws Exception < if(sendMessage.getChatId() == null) throw new Exception("Id must be not null"); Answer answer = new Answer(); answer.setBotApiMethod(sendMessage); return answer; >>
Вот теперь можем написать Handler и Command.
@Component public class CommandHandler extends AbstractHandler < private HashMaphashMap = new HashMap<>(); @Override protected HashMap createMap() < return hashMap; >@Override public TelegramType getHandleType() < return TelegramType.Command; >@Override public int priority() < return 1; >@Override public boolean condition(User user, ClassifiedUpdate update) < return hashMap.containsKey(update.getCommandName()); >@Override public Answer getAnswer(User user, ClassifiedUpdate update) < return hashMap.get(update.getCommandName()).getAnswer(update, user); >>
@Component public class StartCommand implements Command < @Override public Class handler() < return CommandHandler.class; >@Override public Object getFindBy() < return "/start"; >@SneakyThrows @Override public Answer getAnswer(ClassifiedUpdate update, User user) < return new SendMessageBuilder().chatId(user.getChatId()).message("Hello!").build(); >>
Я постарался сделать практическое пособие. Тут нужно много чего дорабатывать.
Код я писал очень давно, поэтому что-то возможно уже нужно обновить, просто решил опубликовать свои наработки в открытый доступ.
В итоге должен получиться простой и расширяемый бот.
Если эта статья вам понравиться, то можно всё допилить и получить невероятно мощную штуку для написания телеграмм ботов, к примеру, выкатить свои аннотации и т.д.
Спасибо за внимание!
- telegram
- telegram bot
- telegram bots
Telegram-бот счётчик сообщений на Java и Spring Boot
Пишем простой Телеграм-бот на Java, который будет подсчитывать сообщения от пользователей чата и записывать их в БД через PostgreSQL.
В этой статье я покажу, как написать 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 дополнительные зависимости для работы с Телеграм ботом и базами данных:
- Telegram Bots
- Hibernate Core Relocation
- PostgreSQL JDBC Driver
- Lombok
В каталоге 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("$") String botName; @Value("$") String token; @Value("$") 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() 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 = ) . Ошибка исчезнет, как только мы добавим информацию для доступа к БД в config.properties .
Добавление кнопок
Чтобы Telegram bot на Java и Spring Boot выглядел по-настоящему серьёзным, давайте добавим ему команду /help и пару кнопок.
Создадим в основной директории проекта пакет components . В него добавим:
1. Интерфейс BotCommands :
public interface BotCommands < ListLIST_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"); ListrowInline = List.of(START_BUTTON, HELP_BUTTON); List 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 . В нём следует создать:
@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
Данный интерфейс нам нужен для удобной работы с 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 и небольшого пет-проекта, который можно представить в своём резюме, такая программа вполне подойдёт.
Остались вопросы? Задавайте их в комментариях к этой статье.