SpringBoot Starter Telegram Mapping для вашего бота

в 3:15, , рубрики: springboot, telegram, telegram bots

Вдохновившись различными проектами по переделке получения тележных сообщений в более привычный Spring MVC-ый формат, решил написать про легковесный стартер. Оглядевшись вокруг, быстро понял, что все текущие реализации, к сожалению, заброшены, за исключением, наверное, самой популярной либы - TelegramBots. Её и решил взять за основу.

Начнем с аннотаций.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BotRequestMapping {

    @AliasFor("value")
    String[] command() default {};

    @AliasFor("command")
    String[] value() default {};
}

Теперь научим эту аннотацию получать сообщения. Если посмотреть на реализацию регистрации бота, там всего 2 бина, которые нужно переопределить - TelegramBotInitializer и TelegramBotsLongPollingApplication. С них и начнём.

@Bean
@ConditionalOnMissingBean(TelegramBotsLongPollingApplication.class)
public TelegramBotsLongPollingApplication telegramBotsApplication() {
    return new TelegramBotsLongPollingApplication();
}
@Bean
@ConditionalOnMissingBean
public TelegramBotInitializer telegramBotInitializer(TelegramBotsLongPollingApplication telegramBotsApplication,
                                                     ObjectProvider<List<SpringLongPollingBot>> longPollingBots) {
    return new TelegramBotInitializer(telegramBotsApplication,
            longPollingBots.getIfAvailable(Collections::emptyList));
}

Отлично, теперь нам нужно зарегистрировать своего бота. Добавим ещё один бин.

@Bean
public SpringLongPollingBot springLongPollingBot(TelegramBotProperties properties,
                                                 BotRequestProcessor botRequestProcessor) {
    return new SpringLongPollingBot() {
        @Override
        public String getBotToken() {
            return properties.getToken();
        }
        @Override
        public LongPollingUpdateConsumer getUpdatesConsumer() {
            return updates -> updates.forEach(botRequestProcessor::handleUpdate);
        }
    };
}

Создаем реализацию SpringLongPollingBot и реализуем два метода - отдаём токен и получаем апдейты. Далее, уже привычно, отдаём апдейты на съедение BeanPostProcessor-у.

@Slf4j
@Component
public class BotRequestProcessor implements BeanPostProcessor {

    private final Map<String, BotHandlerMethod> handlers = new HashMap<>();

    @Override
    public Object postProcessAfterInitialization(Object bean, @NotNull String beanName) {
        for (Method method : bean.getClass().getDeclaredMethods()) { // Ищем нашу аннотцию
            BotRequestMapping annotation = AnnotationUtils.findAnnotation(method, BotRequestMapping.class);
            if (annotation != null) {
                for (String command : annotation.value()) { // Берем все команды из аннотации
                    handlers.put(command, new BotHandlerMethod(bean, method)); // И сохраняем
                    log.debug("✅ Registered command handler '{}': {}.{}", command, bean.getClass().getSimpleName(), method.getName());
                }
            }
        }
        return bean;
    }

    public void handleUpdate(Update update) {
        if (update.hasMessage() && update.getMessage().hasText()) {  // Проверяем, что сообщение текстовое
            String text = update.getMessage().getText().trim();
            BotHandlerMethod handlerMethod = handlers.get(text); // Ищем обработчик команды
            if (handlerMethod != null) {
                try {
                    handlerMethod.invoke(update); // Вызываем нужный метод
                } catch (Exception e) {
                    log.error("Error on calling handler '{}': {}", text, e.getMessage(), e);
                }
            } else {
                log.warn("No handler found for: {}", text);
            }
        }
    }

    private record BotHandlerMethod(Object bean, Method method) {

        void invoke(Update update) throws Exception {
                method.invoke(bean, update); // Вызываем метод и передаем в него update
            }
        }
}

Уверен многие написали уже не один спринговый бин-пост-процессор, но всё же оставил комментарии для самых маленьких.

Теперь сделаем из этого стартер. Создадим папку в сорсах META-INF/spring и положим туда путь до наших конфигураций.

com.flux.spring.boot.telegram.mapping.config.TelegramBotAutoConfiguration

Стартер готов. Как пользоваться?

Добавляем зависимость.

<dependency>
    <groupId>com.flux</groupId>
    <artifactId>spring-boot-telegram-mapping</artifactId>
</dependency>

И прописываем токен бота в application.properties/yml.

telegram:
  bot:
    token: "secret"

Теперь вы счастливый обладатель маппинга в стиле Srping MVC.

@BotRequestMapping("/start")
public void startCommand(Update update) {
    log.info("/start {}", update.getMessage().getFrom().getUserName());
}

Конечно, можно пойти дальше. Добавить функционал для отправки сообщений, поддержку Webhook-ов, различных типов команд и прочего. Но это уже совсем другая история.

Итого у нас есть легковесный стартер написанный на основе популярной, а главное живой библиотеке.

Ссылка на полный код тут.
Источники вдохновения: раз, два.

Автор: broadcastination

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js