⬆️ ⬇️

Developing a telegram bot using Spring

Write bots telegrams? Does your development productivity want the best? Looking for something new? Then I ask for cat.





The idea is the following: slyamzit spring mvc architecture and transfer to telegram api.

It should look something like this:



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


or



Bins example
 @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()) ); } } 


This gives the following benefits:





Let's now see how this can be started in our project.



Annotations
 @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}; } 


Create your handler container in the form of a regular HashMap



Container
 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(); } } 


We will store the wrapper controllers in the container (for a pair of @BotController and @BotRequestMapping)



Controller wrapper
 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; } } 


Now, when we have this codebase, the question arises: how can Spring make the container automatically fill so that we can use it?



To do this, we implement a special bin - BeanPostProcessor. This makes it possible to catch bins during their initialization. Our controllers have scope by default - singleton, it means they will be initialized with the start of the context!



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; } } 


Initialize the context in which all our bins are registered and - voila! You can select handlers for messages, for example, like this:



Handler selection
 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(); } } 


P.S

Telegram is developing very rapidly. Using bots, we can organize our stores, give commands to our various Internet things, organize blog feeds and much more. And most importantly, all this in a single application!



References:





')

Source: https://habr.com/ru/post/335490/



All Articles