Перейти к содержимому

Как написать telegram бота на java

  • автор:

Простой 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 в двух режимах:

  1. Long Polling (Длинные опросы) — необходимо наследоваться от класса org.telegram.telegrambots.bots.TelegramLongPollingBot
  2. 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:

Структура папки 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:

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-бот готов! Осталось только запустить приложение и радоваться жизни:

Пишем телеграм бота на Java от А до Я

У меня появилась идея написать серию статей по созданию полноценного проекта на Java для тех, у кого нет опыта работы. Я хочу научить и показать процесс разработки максимально детально, насколько это вообще можно. Шаг за шагом идти к намеченной цели.
Мы будем двигаться с двух сторон — я буду стараться научить, а вы будете стараться понять и воспроизвести у себя это.

Серия статей похожа на пошаговую инструкцию что нужно и делать и как чтобы добиться цели. Поэтому будет разумно вместе со мной пройти этот путь и написать такой же или похожий проект для себя.

Кому это будет интересно?

Целевая аудитория — все, кто знает Java SE. Все остальное я буду или описывать в рамках этой серии статей, или это уже описано мною в других статья ранее (например работу с Гитом я описал заранее и просто прикрепляю ссылку на него).
Разумеется, это может быть интересно и уже опытным людям. Отдельно буду благодарен за конструктивную критику моей реализации (это можно будет сделать через гитхаб, в рамках нового issue или написать в телеграм-канале).

Во время разработки проекта столкнемся с проблемами, посмотрим как их можно решить. Напишем документацию. Отрефакторим код. Покроем все качественными тестами. Настроим и добавим метрики по отслеживанию качества кода в CI процесс.

Что будем в этом проекте?
Понятно, что всем интересно поработать с какой-то технологией, базой данных, их интеграцией. Много будет уделено вниманию развертывания проекта. Его тестированию. Разработка ведется в открытом репозитории на гитхабе. Для этого дела, создал отдельную организацию, чтобы объединить все необходимые для работы проекта данные.
Поговорим о:
— Поговорим о том, как спроектировать проект, написать задачи для него. Порисуем схемы базы данных.
— SQL/MySQL — для этого отведена первая серия статей. Описание рассчитано на людей, которые вообще не имеют об этом понятия.
— Flyway — через flyway будем работать с версионированием базы данных
— SpingBoot — как каркас всего приложения. Здесь внутри затронем Scheduling для наших нужд.
— Docker / Docker-compose — при помощи докера и докер компоуза будем развертывать наше приложение. Вся необходимая инфраструктура будет разворачиваться при запуске одной строки(!!)
— Bash Script — напишем два простеньких баш скрипта, которые соберут воедино наш процесс развертывания
— Lombok — просто как удобная вещь для наших сущностей.
— Тестирование — отдельно хорошо поговорим о тестировании, о модульных тестах, интеграционных. О создании CI процесса для разработки, настройки этого процесса.

Что будет в итоге?
В итоге получим реальный проект, который будет работать. Его можно будет потрогать, каждый шаг его разработки будет тщательно описан.

Для освещения деятельности создал телеграм-канал: t.me/romankh3
В нем я размещаю свою open-source деятельность и львиная его доля на данный момент — это написание проекта от А до Я. Поэтому всех, кому будет интересно следить за проектом — приглашаю присоединиться.

Сразу хочу сказать, что это не реклама JavaRush. Просто так исторически сложилось, что я там публикую свои статьи для начинающих специалистов. Все материалы, которые будут нужны (статьи, исходники кода) — все в открытом доступе.

Если это интересно для тебя — присоединяйся.
Вот первая статья этой серии: javarush.ru/. ​ihy-proekt-dlja-portfolio
Остальные ссылки находятся в статьях.

Всем добра. Надеюсь этот проект поможет кому-то еще.

UPDATE: в спешке донести идею о том, что я создаю я как-то не подумал, что это все реклама. Спасибо в комментах написали, усвоил. Поэтому как только я закомчу свою серию статей (там всего будет где-то 20-25 полноценных статей) по телеграм-боту и проект уже будет готов — напишу сжато 2-3 части здесь, на хабре.

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

Пример использования Java-библиотеки для создания Telegram-бота

Каждый разработчик (и не только), который использует Telegram в повседневной жизни, хотя бы раз задумывался о том, каково это — создать своего бота, на сколько это сложно и какой язык программирования лучше использовать.

На все эти вопросы могу дать самый простой (и, наверно, самый правильный) ответ: все зависит от вас самих, ваших знаний и намерений.

… Но в этой небольшой статье, я покажу, как можно создать своего бота на языке Java и что это довольно интересно и несложно.

Мы будем использовать библиотеку для работы с Telegram Bots API и ее расширение, позволяющее создавать свои команды (‘ /custom_cmd ‘) и обрабатывать их простым способом.

Задачей бота будет являться регистрация пользователя и отправка сообщения от указанного им имени другим пользователям бота.

Создание нового проекта и подготовка

1. Добавление зависимостей в проект

Создадим новый maven-проект и отредактируем pom.xml , добавив в него необходимые зависимости:

pom.xml

   4.0.0 io.example anonymizerbot 1.0-SNAPSHOT   org.apache.maven.plugins maven-compiler-plugin ```8 8       org.telegram telegrambots LATEST  org.telegram telegrambotsextensions LATEST  . 

Telegram API — библиотека для работы с Telegram Bots API, содержит в себе классы и методы для взаимодействия с сервисами Telegram и некоторые расширения этих классов.

2. Создание аккаунта для бота

Для этого нам необходимо обратиться за помощью к боту BotFather:

  • найдем бота в поиске;
  • выполним команду «/start»;
  • выполним команду «/newbot»;
  • зададим какое-нибудь имя нашему боту (должно заканчиваться на «Bot»). Я назвал его «ExampleOfAnonymizerBot».

После выполнения этих команд мы получим токен, который нам понадобится для использования Bot API. (7xxxxxxx2:Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0)



Реализация

1. Модель анонимного отправителя сообщений

Данные, необходимые нам от каждого пользователя:

  • User mUser — информация о пользователе Telegram;
  • Chat mChat — информация о чате пользователя и бота;
  • String mDisplayedName — имя, от которого пользователь будет посылать сообщения другим пользователям бота.

Anonymous.java

package io.example.anonymizerbot.model; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.telegram.telegrambots.meta.api.objects.Chat; import org.telegram.telegrambots.meta.api.objects.User; public final class Anonymous < private static final Logger LOG = LogManager.getLogger(Anonymous.class); private static final String USER_CHAT_CANNOT_BE_NULL = "User or chat cannot be null!"; private final User mUser; private final Chat mChat; private String mDisplayedName; public Anonymous(User user, Chat chat) < if (user == null || chat == null) < LOG.error(USER_CHAT_CANNOT_BE_NULL); throw new IllegalStateException(USER_CHAT_CANNOT_BE_NULL); >mUser = user; mChat = chat; > @Override public int hashCode() < return mUser.hashCode(); >@Override public boolean equals(Object obj) < return obj instanceof Anonymous && ((Anonymous) obj).getUser().equals(mUser); >public User getUser() < return mUser; >public Chat getChat() < return mChat; >public String getDisplayedName() < return mDisplayedName; >public void setDisplayedName(String displayedName) < mDisplayedName = displayedName; >>

Добавим сервис, содержащий часто используемые методы для манипулирования множеством анонимных пользователей.

AnonymousService.java

package io.example.anonymizerbot.service; import io.example.anonymizerbot.model.Anonymous; import org.telegram.telegrambots.meta.api.objects.User; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.stream.Stream; public final class AnonymousService < private final SetmAnonymouses; public AnonymousService() < mAnonymouses = new HashSet<>(); > public boolean setUserDisplayedName(User user, String name) < if (!isDisplayedNameTaken(name)) < mAnonymouses.stream().filter(a ->a.getUser().equals(user)).forEach(a -> a.setDisplayedName(name)); return true; > return false; > public boolean removeAnonymous(User user) < return mAnonymouses.removeIf(a ->a.getUser().equals(user)); > public boolean addAnonymous(Anonymous anonymous) < return mAnonymouses.add(anonymous); >public boolean hasAnonymous(User user) < return mAnonymouses.stream().anyMatch(a ->a.getUser().equals(user)); > public String getDisplayedName(User user) < Anonymous anonymous = mAnonymouses.stream().filter(a ->a.getUser().equals(user)).findFirst().orElse(null); if (anonymous == null) < return null; >return anonymous.getDisplayedName(); > public Stream anonymouses() < return mAnonymouses.stream(); >private boolean isDisplayedNameTaken(String name) < return mAnonymouses.stream().anyMatch(a ->Objects.equals(a.getDisplayedName(), name)); > >

2. Интерфейс бота

Любая кастомная команда должна наследоваться от BotCommand и реализовывать метод
execute(AbsSender sender, User user, Chat chat, String[] strings) , который используется для обработки команд пользователей.

После того как мы обработаем команду пользователя, мы можем послать ему ответ, используя метод execute класса AbsSender , который принимает на вход вышеупомянутый execute(AbsSender sender, User user, Chat chat, String[] strings) .

Здесь и далее чтобы не оборачивать каждый раз метод AbsSender.execute , который может выбросить исключение TelegramApiException , в try-catch , и для того чтобы не прописывать в каждой команде вывод однообразных логов, создадим класс AnonymizerCommand , а наши кастомные команды будем уже наследовать от него (обработку исключений в этом примере оставим):

AnonymizerCommand.java

package io.example.anonymizerbot.command; import io.example.anonymizerbot.logger.LogLevel; import io.example.anonymizerbot.logger.LogTemplate; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import org.telegram.telegrambots.extensions.bots.commandbot.commands.BotCommand; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.bots.AbsSender; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; abstract class AnonymizerCommand extends BotCommand < final Logger log = LogManager.getLogger(getClass()); AnonymizerCommand(String commandIdentifier, String description) < super(commandIdentifier, description); >void execute(AbsSender sender, SendMessage message, User user) < try < sender.execute(message); log.log(Level.getLevel(LogLevel.SUCCESS.getValue()), LogTemplate.COMMAND_SUCCESS.getTemplate(), user.getId(), getCommandIdentifier()); >catch (TelegramApiException e) < log.error(LogTemplate.COMMAND_EXCEPTION.getTemplate(), user.getId(), getCommandIdentifier(), e); >> >

Определим команды, на которые наш бот будет реагировать:

  • /start — создаст нового Anonymous без имени и добавит его в коллекцию Anonymouses ;

StartCommand.java

package io.example.anonymizerbot.command; import io.example.anonymizerbot.logger.LogLevel; import io.example.anonymizerbot.logger.LogTemplate; import io.example.anonymizerbot.model.Anonymous; import io.example.anonymizerbot.service.AnonymousService; import org.apache.logging.log4j.Level; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Chat; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.bots.AbsSender; public final class StartCommand extends AnonymizerCommand < private final AnonymousService mAnonymouses; // обязательно нужно вызвать конструктор суперкласса, // передав в него имя и описание команды public StartCommand(AnonymousService anonymouses) < super("start", "start using bot\n"); mAnonymouses = anonymouses; >/** * реализованный метод класса BotCommand, в котором обрабатывается команда, введенная пользователем * @param absSender - отправляет ответ пользователю * @param user - пользователь, который выполнил команду * @param chat - чат бота и пользователя * @param strings - аргументы, переданные с командой */ @Override public void execute(AbsSender absSender, User user, Chat chat, String[] strings) < log.info(LogTemplate.COMMAND_PROCESSING.getTemplate(), user.getId(), getCommandIdentifier()); StringBuilder sb = new StringBuilder(); SendMessage message = new SendMessage(); message.setChatId(chat.getId().toString()); if (mAnonymouses.addAnonymous(new Anonymous(user, chat))) < log.info("User <>is trying to execute '<>' the first time. Added to users' list.", user.getId(), getCommandIdentifier()); sb.append("Hi, ").append(user.getUserName()).append("! You've been added to bot users' list!\n") .append("Please execute command:\n'/set_name '\nwhere <displayed_name> is the name you want to use to hide your real name."); > else < log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>has already executed '<>'. Is he trying to do it one more time?", user.getId(), getCommandIdentifier()); sb.append("You've already started bot! You can send messages if you set your name (/set_name)."); > message.setText(sb.toString()); execute(absSender, message, user); > >
  • /help — выведет пользователю информацию обо всех доступных командах (конструктор отличается от других тем, что в него необходимо передать ICommandRegistry , который содержит все кастомные команды);

HelpCommand.java

package io.example.anonymizerbot.command; import io.example.anonymizerbot.logger.LogTemplate; import org.telegram.telegrambots.extensions.bots.commandbot.commands.ICommandRegistry; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Chat; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.bots.AbsSender; public final class HelpCommand extends AnonymizerCommand < private final ICommandRegistry mCommandRegistry; public HelpCommand(ICommandRegistry commandRegistry) < super("help", "list all known commands\n"); mCommandRegistry = commandRegistry; >@Override public void execute(AbsSender absSender, User user, Chat chat, String[] strings) < log.info(LogTemplate.COMMAND_PROCESSING.getTemplate(), user.getId(), getCommandIdentifier()); StringBuilder helpMessageBuilder = new StringBuilder("Available commands:"); mCommandRegistry.getRegisteredCommands().forEach(cmd -> helpMessageBuilder.append(cmd.toString()).append("\n")); SendMessage helpMessage = new SendMessage(); helpMessage.setChatId(chat.getId().toString()); helpMessage.enableHtml(true); helpMessage.setText(helpMessageBuilder.toString()); execute(absSender, helpMessage, user); > >
  • /set_name — задаст пользователю имя, от которого будут отправляться анонимные сообщения;

SetNameCommand.java

package io.example.anonymizerbot.command; import io.example.anonymizerbot.logger.LogLevel; import io.example.anonymizerbot.logger.LogTemplate; import io.example.anonymizerbot.service.AnonymousService; import org.apache.logging.log4j.Level; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Chat; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.bots.AbsSender; public final class SetNameCommand extends AnonymizerCommand < private final AnonymousService mAnonymouses; public SetNameCommand(AnonymousService anonymouses) < super("set_name", "set or change name that will be displayed with your messages\n"); mAnonymouses = anonymouses; >@Override public void execute(AbsSender absSender, User user, Chat chat, String[] strings) < log.info(LogTemplate.COMMAND_PROCESSING.getTemplate(), user.getId(), getCommandIdentifier()); SendMessage message = new SendMessage(); message.setChatId(chat.getId().toString()); if (!mAnonymouses.hasAnonymous(user)) < log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to execute '<>' without starting the bot!", user.getId(), getCommandIdentifier()); message.setText("Firstly you should start the bot! Execute '/start' command!"); execute(absSender, message, user); return; > String displayedName = getName(strings); if (displayedName == null) < log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to set empty name.", user.getId()); message.setText("You should use non-empty name!"); execute(absSender, message, user); return; > StringBuilder sb = new StringBuilder(); if (mAnonymouses.setUserDisplayedName(user, displayedName)) < if (mAnonymouses.getDisplayedName(user) == null) < log.info("User <>set a name '<>'", user.getId(), displayedName); sb.append("Your displayed name: '").append(displayedName) .append("'. Now you can send messages to bot!"); > else < log.info("User <>has changed name to '<>'", user.getId(), displayedName); sb.append("Your new displayed name: '").append(displayedName).append("'."); > > else < log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to set taken name '<>'", user.getId(), displayedName); sb.append("Name ").append(displayedName).append(" is already in use! Choose another name!"); > message.setText(sb.toString()); execute(absSender, message, user); > private String getName(String[] strings) < if (strings == null || strings.length == 0) < return null; >String name = String.join(" ", strings); return name.replaceAll(" ", "").isEmpty() ? null : name; > >
  • /my_name — отобразит текущее имя пользователя;

MyNameCommand.java

package io.example.anonymizerbot.command; import io.example.anonymizerbot.logger.LogLevel; import io.example.anonymizerbot.logger.LogTemplate; import io.example.anonymizerbot.service.AnonymousService; import org.apache.logging.log4j.Level; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Chat; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.bots.AbsSender; public final class MyNameCommand extends AnonymizerCommand < private final AnonymousService mAnonymouses; public MyNameCommand(AnonymousService anonymouses) < super("my_name", "show your current name that will be displayed with your messages\n"); mAnonymouses = anonymouses; >@Override public void execute(AbsSender absSender, User user, Chat chat, String[] strings) < log.info(LogTemplate.COMMAND_PROCESSING.getTemplate(), user.getId(), getCommandIdentifier()); StringBuilder sb = new StringBuilder(); SendMessage message = new SendMessage(); message.setChatId(chat.getId().toString()); if (!mAnonymouses.hasAnonymous(user)) < sb.append("You are not in bot users' list! Send /start command!"); log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to execute '<>' without starting the bot.", user.getId(), getCommandIdentifier()); > else if(mAnonymouses.getDisplayedName(user) == null) < sb.append("Currently you don't have a name.\nSet it using command:\n'/set_name <displayed_name>'"); log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to execute '<>' without having a name.", user.getId(), getCommandIdentifier()); > else < log.info("User <>is executing '<>'. Name is '<>'.", user.getId(), getCommandIdentifier(), mAnonymouses.getDisplayedName(user)); sb.append("Your current name: ").append(mAnonymouses.getDisplayedName(user)); > message.setText(sb.toString()); execute(absSender, message, user); > >
  • /stop — удалит пользователя из коллекции анонимусов.

StopCommand.java

package io.example.anonymizerbot.command; import io.example.anonymizerbot.logger.LogLevel; import io.example.anonymizerbot.logger.LogTemplate; import io.example.anonymizerbot.service.AnonymousService; import org.apache.logging.log4j.Level; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Chat; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.bots.AbsSender; public final class StopCommand extends AnonymizerCommand < private final AnonymousService mAnonymouses; public StopCommand(AnonymousService anonymouses) < super("stop", "remove yourself from bot users' list\n"); mAnonymouses = anonymouses; >@Override public void execute(AbsSender absSender, User user, Chat chat, String[] strings) < log.info(LogTemplate.COMMAND_PROCESSING.getTemplate(), user.getId(), getCommandIdentifier()); StringBuilder sb = new StringBuilder(); SendMessage message = new SendMessage(); message.setChatId(chat.getId().toString()); if (mAnonymouses.removeAnonymous(user)) < log.info("User <>has been removed from users list!", user.getId()); sb.append("You've been removed from bot's users list! Bye!"); > else < log.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to execute '<>' without having executed 'start' before!", user.getId(), getCommandIdentifier()); sb.append("You were not in bot users' list. Bye!"); > message.setText(sb.toString()); execute(absSender, message, user); > >

3. Инициализация и запуск бота

Класс бота, в котором производится регистрация всех кастомных команд, обработчика сообщений-не команд и неизвестных команд.

AnonymizerBot.java

package io.example.anonymizerbot.bot; import io.example.anonymizerbot.command.*; import io.example.anonymizerbot.logger.LogLevel; import io.example.anonymizerbot.logger.LogTemplate; import io.example.anonymizerbot.model.Anonymous; import io.example.anonymizerbot.service.AnonymousService; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.telegram.telegrambots.bots.DefaultBotOptions; import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot; import org.telegram.telegrambots.meta.api.methods.send.SendMessage; import org.telegram.telegrambots.meta.api.objects.Message; import org.telegram.telegrambots.meta.api.objects.Update; import org.telegram.telegrambots.meta.api.objects.User; import org.telegram.telegrambots.meta.exceptions.TelegramApiException; import java.util.stream.Stream; public final class AnonymizerBot extends TelegramLongPollingCommandBot < private static final Logger LOG = LogManager.getLogger(AnonymizerBot.class); // имя бота, которое мы указали при создании аккаунта у BotFather // и токен, который получили в результате private static final String BOT_NAME = "AnonymizerBotExample"; private static final String BOT_TOKEN = "7xxxxxxx2:Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx0"; private final AnonymousService mAnonymouses; public AnonymizerBot(DefaultBotOptions botOptions) < super(botOptions, BOT_NAME); LOG.info("Initializing Anonymizer Bot. "); LOG.info("Initializing anonymouses list. "); mAnonymouses = new AnonymousService(); // регистрация всех кастомных команд LOG.info("Registering commands. "); LOG.info("Registering '/start'. "); register(new StartCommand( mAnonymouses)); LOG.info("Registering '/set_name'. "); register(new SetNameCommand(mAnonymouses)); LOG.info("Registering '/stop'. "); register(new StopCommand(mAnonymouses)); LOG.info("Registering '/my_name'. "); register(new MyNameCommand(mAnonymouses)); HelpCommand helpCommand = new HelpCommand(this); LOG.info("Registering '/help'. "); register(helpCommand); // обработка неизвестной команды LOG.info("Registering default action'. "); registerDefaultAction(((absSender, message) -> < LOG.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to execute unknown command '<>'.", message.getFrom().getId(), message.getText()); SendMessage text = new SendMessage(); text.setChatId(message.getChatId()); text.setText(message.getText() + " command not found!"); try < absSender.execute(text); >catch (TelegramApiException e) < LOG.error("Error while replying unknown command to user <>.", message.getFrom(), e); > helpCommand.execute(absSender, message.getFrom(), message.getChat(), new String[] <>); >)); > @Override public String getBotToken() < return BOT_TOKEN; >// обработка сообщения не начинающегося с '/' @Override public void processNonCommandUpdate(Update update) < LOG.info("Processing non-command update. "); if (!update.hasMessage()) < LOG.error("Update doesn't have a body!"); throw new IllegalStateException("Update doesn't have a body!"); >Message msg = update.getMessage(); User user = msg.getFrom(); LOG.info(LogTemplate.MESSAGE_PROCESSING.getTemplate(), user.getId()); if (!canSendMessage(user, msg)) < return; >String clearMessage = msg.getText(); String messageForUsers = String.format("%s:\n%s", mAnonymouses.getDisplayedName(user), msg.getText()); SendMessage answer = new SendMessage(); // отправка ответа отправителю о том, что его сообщение получено answer.setText(clearMessage); answer.setChatId(msg.getChatId()); replyToUser(answer, user, clearMessage); // отправка сообщения всем остальным пользователям бота answer.setText(messageForUsers); Stream anonymouses = mAnonymouses.anonymouses(); anonymouses.filter(a -> !a.getUser().equals(user)) .forEach(a -> < answer.setChatId(a.getChat().getId()); sendMessageToUser(answer, a.getUser(), user); >); > // несколько проверок, чтобы можно было отправлять сообщения другим пользователям private boolean canSendMessage(User user, Message msg) < SendMessage answer = new SendMessage(); answer.setChatId(msg.getChatId()); if (!msg.hasText() || msg.getText().trim().length() == 0) < LOG.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to send empty message!", user.getId()); answer.setText("You shouldn't send empty messages!"); replyToUser(answer, user, msg.getText()); return false; > if(!mAnonymouses.hasAnonymous(user)) < LOG.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to send message without starting the bot!", user.getId()); answer.setText("Firstly you should start bot! Use /start command!"); replyToUser(answer, user, msg.getText()); return false; > if (mAnonymouses.getDisplayedName(user) == null) < LOG.log(Level.getLevel(LogLevel.STRANGE.getValue()), "User <>is trying to send message without setting a name!", user.getId()); answer.setText("You must set a name before sending messages.\nUse '/set_name ' command."); replyToUser(answer, user, msg.getText()); return false; > return true; > private void sendMessageToUser(SendMessage message, User receiver, User sender) < try < execute(message); LOG.log(Level.getLevel(LogLevel.SUCCESS.getValue()), LogTemplate.MESSAGE_RECEIVED.getTemplate(), receiver.getId(), sender.getId()); >catch (TelegramApiException e) < LOG.error(LogTemplate.MESSAGE_LOST.getTemplate(), receiver.getId(), sender.getId(), e); >> private void replyToUser(SendMessage message, User user, String messageText) < try < execute(message); LOG.log(Level.getLevel(LogLevel.SUCCESS.getValue()), LogTemplate.MESSAGE_SENT.getTemplate(), user.getId(), messageText); >catch (TelegramApiException e) < LOG.error(LogTemplate.MESSAGE_EXCEPTION.getTemplate(), user.getId(), e); >> >

Наконец, запуск бота:

BotInitializer.java

package io.example.anonymizerbot; import io.example.anonymizerbot.bot.AnonymizerBot; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.telegram.telegrambots.ApiContextInitializer; import org.telegram.telegrambots.bots.DefaultBotOptions; import org.telegram.telegrambots.meta.ApiContext; import org.telegram.telegrambots.meta.TelegramBotsApi; import org.telegram.telegrambots.meta.exceptions.TelegramApiRequestException; public final class BotInitializer < private static final Logger LOG = LogManager.getLogger(BotInitializer.class); private static final String PROXY_HOST = "xx.xx.xxx.xxx"; private static final int PROXY_PORT = 9999; public static void main(String[] args) < try < LOG.info("Initializing API context. "); ApiContextInitializer.init(); TelegramBotsApi botsApi = new TelegramBotsApi(); LOG.info("Configuring bot options. "); DefaultBotOptions botOptions = ApiContext.getInstance(DefaultBotOptions.class); botOptions.setProxyHost(PROXY_HOST); botOptions.setProxyPort(PROXY_PORT); botOptions.setProxyType(DefaultBotOptions.ProxyType.SOCKS4); LOG.info("Registering Anonymizer. "); botsApi.registerBot(new AnonymizerBot(botOptions)); LOG.info("Anonymizer bot is ready for work!"); >catch (TelegramApiRequestException e) < LOG.error("Error while initializing bot!", e); >> >

Вот и все! Бот готов к первым тестам.

Пример использования

В качестве примера рассмотрим следующий сценарий:

  1. ‘A’ начинает работу с ботом ( /start );
  2. ‘A’ пытается отправить сообщение;
  3. ‘A’ пытается задать имя ( /set_name );
  4. ‘A’ задает имя ( /set_name Pendalf );
  5. ‘A’ посылает сообщение другим пользователям (которых нет);
  6. ‘B’ начинает работу с ботом ( /start );
  7. ‘B’ задает имя ( /set_name Chuck Norris );
  8. ‘B’ посылает сообщение другим пользователям;
  9. ‘A’ видит сообщение от ‘B’ и посылает сообщение в ответ;
  10. ‘B’ видит ответ от ‘A’ и больше ему не пишет.



Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *