Как стать автором
Обновить

Разработка telegram бота с использованием Spring

Время на прочтение6 мин
Количество просмотров39K

Пишете телеграмм ботов? Ваша производительность разработки желает лучшего? Ищете чего-то нового? Тогда прошу под кат.



Идея заключается в следующем: слямзить архитектуру spring mvc и перенести на telegram api.
Выглядеть должно как-то так:


@BotController
public class SimpleOkayController {
    @BotRequestMapping(value = "/ok")
    public SendMessage ok(Update update) {
        return new SendMessage()
                .setChatId(update.getMessage().getChatId())
                .setText("okay bro, okay!");
    }
}

или


Пример с бинами
@BotController
public class StartController {
    @Autowired
    private Filter shopMenu;

    @Autowired
    private PayTokenService payTokenService;

    @Autowired
    private ItemService itemService;

    @BotRequestMapping("/shop")
    public SendMessage generateInitMenu(Update update) {
            return  new SendMessage()
                    .setChatId(update.getMessage().getChatId().toString())
                    .setText("Товары моего магазинчика!")
                    .setReplyMarkup(shopMenu.getSubMenu(0L, 4L, 1L)); // <--
    }

    @BotRequestMapping(value = "/buyItem", method = BotRequestMethod.EDIT)
    public List<BotApiMethod> bayItem(Update update) {
        ....................
        Item item = itemService.findById(id); // <--

        return Arrays.asList(new EditMessageText()
                .setChatId(update.getMessage().getChatId())
                .setMessageId(update.getMessage().getMessageId())
                .setText("Подтвердите ваш выбор, в форме ниже"),

                new SendInvoice()
                        .setChatId(Integer.parseInt(update.getMessage().getChatId().toString()))
                        .setDescription(item.getDescription())
                        .setTitle(item.getName())
                        .setProviderToken(payTokenService.getPayToken())
                        ........................
                        .setPrices(item.getPrice())
        );
    }

}

Это даёт следующие преимущества:


  • Не надо писать кастомную логику для выбора обработчика сообщения от пользователя
  • Возможность инжектить разлиные бины в наш @BotController
  • Как следствие из предыдущих двух пунктов — существенное сокращение объемов кода
  • Потенциально (хотя я этого еще не сделал) аргументы кастомного метода обработчика, могут быть выражены в виде тех аргументов, которые действительно нужны!
  • Возможность создавать серьезные энтерпрайз решения, используя spring

Давайте теперь посмотрим как это можно завести в нашем проекте


Аннотации
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface BotController {
    String[] value() default {};
}

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface BotRequestMapping {
    String[] value() default {};
    BotRequestMethod[] method() default {BotRequestMethod.MSG};
}

Создаем свой контейнер обработчиков в виде обычной HashMap


Контейнер
public class BotApiMethodContainer {
    private static final Logger LOGGER = Logger.getLogger(BotApiMethodContainer.class);

    private Map<String, BotApiMethodController> controllerMap;

    public static BotApiMethodContainer getInstanse() {
        return Holder.INST;
    }

    public void addBotController(String path, BotApiMethodController controller) {
        if(controllerMap.containsKey(path)) throw new BotApiMethodContainerException("path " + path + " already add");
        LOGGER.trace("add telegram bot controller for path: " +  path);
        controllerMap.put(path, controller);
    }

    public BotApiMethodController getBotApiMethodController(String path) {
        return controllerMap.get(path);
    }

    private BotApiMethodContainer() {
        controllerMap = new HashMap<>();
    }

    private static class Holder{
        final static BotApiMethodContainer INST = new BotApiMethodContainer();
    }
}

В контейнере будем хранить контроллеры обертки (для пары @BotController и @BotRequestMapping)


Контроллер обертка
public abstract class BotApiMethodController {
    private static final Logger LOGGER = Logger.getLogger(BotApiMethodController.class);

    private Object bean;
    private Method method;
    private Process processUpdate;

    public BotApiMethodController(Object bean, Method method) {
        this.bean = bean;
        this.method = method;

        processUpdate = typeListReturnDetect() ? this::processList : this::processSingle;
    }

    public abstract boolean successUpdatePredicate(Update update);

    public List<BotApiMethod> process(Update update) {
        if(!successUpdatePredicate(update)) return null;

        try {
            return processUpdate.accept(update);
        } catch (IllegalAccessException | InvocationTargetException e) {
            LOGGER.error("bad invoke method", e);
        }

        return null;
    }

    boolean typeListReturnDetect() {
        return List.class.equals(method.getReturnType());
    }

    private List<BotApiMethod> processSingle(Update update) throws InvocationTargetException, IllegalAccessException {
        BotApiMethod botApiMethod = (BotApiMethod) method.invoke(bean, update);
        return botApiMethod != null ? Collections.singletonList(botApiMethod) : new ArrayList<>(0);
    }

    private List<BotApiMethod> processList(Update update) throws InvocationTargetException, IllegalAccessException {
        List<BotApiMethod> botApiMethods = (List<BotApiMethod>) method.invoke(bean, update);
        return botApiMethods != null ? botApiMethods : new ArrayList<>(0);
    }

    private interface Process{
        List<BotApiMethod> accept(Update update) throws InvocationTargetException, IllegalAccessException;
    }
}

Теперь когда у нас есть данная кодовая база возникает вопрос: как Spring заставить автоматически наполнять контейнер, чтобы мы могли им пользоваться?


Для этого реализуем специальный бин — BeanPostProcessor. Это дает возможность отлавливать бины во время их инициализации. Наши контроллеры имеют scope по умолчанию — синглтон, значит инициализироваться они будут со стартом контекста!


TelegramUpdateHandlerBeanPostProcessor
@Component
public class TelegramUpdateHandlerBeanPostProcessor implements BeanPostProcessor, Ordered {
    private static final Logger LOGGER = Logger.getLogger(TelegramUpdateHandlerBeanPostProcessor.class);

    private BotApiMethodContainer container = BotApiMethodContainer.getInstanse();
    private Map<String, Class> botControllerMap = new HashMap<>();

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        if (beanClass.isAnnotationPresent(BotController.class))
            botControllerMap.put(beanName, beanClass);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(!botControllerMap.containsKey(beanName)) return bean;

        Object original = botControllerMap.get(beanName); 
        Arrays.stream(original.getClass().getMethods())
                .filter(method -> method.isAnnotationPresent(BotRequestMapping.class))
                .forEach((Method method) -> generateController(bean, method));
        return bean;
    }

    private void generateController(Object bean, Method method) {
        BotController botController = bean.getClass().getAnnotation(BotController.class);
        BotRequestMapping botRequestMapping = method.getAnnotation(BotRequestMapping.class);

        String path = (botController.value().length != 0 ? botController.value()[0] : "")
                    + (botRequestMapping.value().length != 0 ? botRequestMapping.value()[0] : "");

        BotApiMethodController controller = null;

        switch (botRequestMapping.method()[0]){
            case MSG:
                controller = createControllerUpdate2ApiMethod(bean, method);
                break;
            case EDIT:
                controller = createProcessListForController(bean, method);
                break;
            default:
                break;
        }

        if (controller != null) {
            container.addBotController(path, controller);
        }
    }

    private BotApiMethodController createControllerUpdate2ApiMethod(Object bean, Method method){
        return new BotApiMethodController(bean, method) {
            @Override
            public boolean successUpdatePredicate(Update update) {
                return update!=null && update.hasMessage() && update.getMessage().hasText();
            }
        };
    }

    private BotApiMethodController createProcessListForController(Object bean, Method method){
        return new BotApiMethodController(bean, method) {
            @Override
            public boolean successUpdatePredicate(Update update) {
                return update!=null && update.hasCallbackQuery() && update.getCallbackQuery().getData() != null;
            }
        };
    }

    @Override
    public int getOrder() {
        return 100;
    }
}

Инициализируем контекст, в котором прописаны все наши бины и — вуаля! Подбирать обработчики для сообщений можно, например, так:


Выбор обработчика
public class SelectHandle {
    private static BotApiMethodContainer container = BotApiMethodContainer.getInstanse();

    public static BotApiMethodController getHandle(Update update) {
        String path;
        BotApiMethodController controller = null;

        if (update.hasMessage() && update.getMessage().hasText()) {
            path = update.getMessage().getText().split(" ")[0].trim();
            controller = container.getControllerMap().get(path);
            if (controller == null) controller = container.getControllerMap().get("");

        } else if (update.hasCallbackQuery()) {
            path = update.getCallbackQuery().getData().split("/")[1].trim();
            controller = container.getControllerMap().get(path);
        }

        return controller != null ? controller : new FakeBotApiMethodController();
    }
}

Постскриптум
Telegram развивается очень стремительно. Используя ботов мы можем организовывать свои магазины, давать команды различным своим интернет-вещам, организовывать блог-каналы и многое многое другое. А самое главное, что всё это в едином приложении!


Ссылки:


Теги:
Хабы:
+15
Комментарии22

Публикации

Истории

Работа

Java разработчик
358 вакансий

Ближайшие события