TelegramBot. A funcionalidade básica. Voa separadamente, costeletas separadamente. (Parte 2)

Continuamos a desenvolver a funcionalidade básica para o bot em telegramas. Nas partes anteriores, discutimos o ponto em que o trabalho do bot em receber mensagens, processar e enviar deve ser dividido. Vamos tentar usar as ferramentas básicas do Java Core para tornar nosso bot multithread e assíncrono. Criaremos uma tarefa que leva muito tempo para processar. Vamos ver como os comandos no telegrama funcionam e como eles precisam ser processados.

Esta é uma continuação da primeira parte do artigo sobre programação de bots para telegramas em Java
Instruções do TelegramBot para criar funcionalidades básicas para o bot. (Parte 1)
Para quem é mais interessante, você é bem-vindo…

Devo dizer imediatamente que, nesta parte, tudo foi adicionado de uma só vez e silenciosamente, analisaremos toda a funcionalidade que permitiu ao bot ser capaz de multithread e por que é necessário.

Como de costume do principal:
Você pode encontrar todo o código pronto para este artigo na ramificação Part2-Handlers no repositório git.
O código está funcionando perfeitamente, basta inclinar, alterar os dados para autorização do bot (nome e token) e executar o método principal na classe App.class.

Observe que esta classe envia uma notificação ao administrador do bot quando o bot inicia que o bot foi iniciado. O ID do administrador do bot também é especificado na classe App.class e, se você não o alterar, seu bot tentará enviar mensagens para mim :)

E mais adiante, analisaremos as mudanças que apareceram após o lançamento da primeira parte.

Processamento de comando


Para começar, vamos lidar com esse conceito do que uma equipe é em geral em um sistema de comunicação com um bot de telegrama. Dependendo das configurações do bot, ele pode ver qualquer mensagem de qualquer formato ou apenas comandos especialmente projetados. Qual é a diferença e
onde você pode conhecer essas opções de mensagem.

  1. Texto sem formatação, mensagens regulares.
    Nesta forma, o bot recebe mensagens quando lhe escrevem no PM. E, no entanto, se nas configurações do bot o modo de privacidade em grupos estiver desativado, o bot começará a ver todas as mensagens completamente. Se essa configuração estiver ativada, quando adicionada ao grupo, o bot verá apenas os comandos endereçados a ele. Como eles se parecem - veja o segundo parágrafo
  2. Equipes especialmente projetadas
    Esses comandos sempre começam com uma barra: /
    Depois disso vem a própria equipe. O texto do comando deve estar sem espaços. Um exemplo:
    / start
    Com este comando, qualquer usuário sempre inicia a comunicação com seu bot. Portanto, de acordo com as regras de boa forma, a resposta a este comando deve ser prescrita.

    Todos os comandos com os quais seu bot sabe trabalhar, é recomendável adicionar à lista de habilidades nas configurações do seu bot. Tudo isso é feito em um telegrama com o @BotFather.

    Ao chamar o comando / myBots, selecione seu bot e, em seguida, o botão "Editar bot"
    Você verá uma janela onde todos os parâmetros do bot serão mostrados e, em seguida, você poderá configurar toda a interface e indicar com quais comandos o bot pode trabalhar.



    Eles são definidos neste formato:



    E depois disso, quando você começar a digitar um comando no seu bot, ele mostrará ajuda com uma lista dos comandos listados:



    E há mais uma nuance. Um grupo pode conter vários bots, e se eles tiverem comandos comuns (e comandos comuns serão obrigatórios, a mesma inicialização e ajuda serão implementadas na maioria dos bots), uma parte será adicionada ao próprio comando, informando a qual bot esse comando pertence. E o comando ficará completamente assim:
    / start @ test_habr_bot

E agora, conhecendo todas essas nuances, vamos criar com você uma opção de processamento que deve entender comandos começando com uma barra e saber como distinguir se o comando é endereçado especificamente ao seu bot ou a outro.

Crie um pacote que conterá as classes responsáveis ​​pelo processamento dos comandos.
pacote com.example.telegrambot.command

Na classe Command, listamos todos os comandos que nosso bot deve entender.

public enum Command { NONE, NOTFORME, NOTIFY, START, HELP, ID } 

Como você viu anteriormente, apontei para o @BotFather que o bot eu deveria ser capaz de entender quatro equipes. Estes serão o início e a ajuda padrão. Nós adicionamos um único ID útil. E mais um, notifique, sobre o qual falarei um pouco mais tarde. E duas equipes NONE e NOTFORME, que nos dirão que a mensagem de texto não é um comando, ou esse comando não é para o nosso bot.

Adicionar outra classe auxiliar ParsedCommand

 import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class ParsedCommand { Command command = Command.NONE; String text=""; } 

Seu principal objetivo é armazenar o resultado da análise de texto em objetos desta classe. Ele conterá apenas o próprio time e todo o texto que vem depois do time.

E escreveremos uma aula separada que analisará as equipes para nós. Classe do analisador

 import javafx.util.Pair; import org.apache.log4j.Logger; public class Parser { private static final Logger log = Logger.getLogger(Parser.class); private final String PREFIX_FOR_COMMAND = "/"; private final String DELIMITER_COMMAND_BOTNAME = "@"; private String botName; public Parser(String botName) { this.botName = botName; } public ParsedCommand getParsedCommand(String text) { String trimText = ""; if (text != null) trimText = text.trim(); ParsedCommand result = new ParsedCommand(Command.NONE, trimText); if ("".equals(trimText)) return result; Pair<String, String> commandAndText = getDelimitedCommandFromText(trimText); if (isCommand(commandAndText.getKey())) { if (isCommandForMe(commandAndText.getKey())) { String commandForParse = cutCommandFromFullText(commandAndText.getKey()); Command commandFromText = getCommandFromText(commandForParse); result.setText(commandAndText.getValue()); result.setCommand(commandFromText); } else { result.setCommand(Command.NOTFORME); result.setText(commandAndText.getValue()); } } return result; } private String cutCommandFromFullText(String text) { return text.contains(DELIMITER_COMMAND_BOTNAME) ? text.substring(1, text.indexOf(DELIMITER_COMMAND_BOTNAME)) : text.substring(1); } private Command getCommandFromText(String text) { String upperCaseText = text.toUpperCase().trim(); Command command = Command.NONE; try { command = Command.valueOf(upperCaseText); } catch (IllegalArgumentException e) { log.debug("Can't parse command: " + text); } return command; } private Pair<String, String> getDelimitedCommandFromText(String trimText) { Pair<String, String> commandText; if (trimText.contains(" ")) { int indexOfSpace = trimText.indexOf(" "); commandText = new Pair<>(trimText.substring(0, indexOfSpace), trimText.substring(indexOfSpace + 1)); } else commandText = new Pair<>(trimText, ""); return commandText; } private boolean isCommandForMe(String command) { if (command.contains(DELIMITER_COMMAND_BOTNAME)) { String botNameForEqual = command.substring(command.indexOf(DELIMITER_COMMAND_BOTNAME) + 1); return botName.equals(botNameForEqual); } return true; } private boolean isCommand(String text) { return text.startsWith(PREFIX_FOR_COMMAND); } } 

Em suma. Ao inicializar o analisador, devemos passar o nome do nosso bot no construtor para que o analisador possa distinguir seus comandos de estranhos.

Bem, então chamamos o método público

 public ParsedCommand getParsedCommand(String text) 

Para o qual passamos o texto da mensagem nos argumentos, e ele deve retornar um comando para nós e o texto da mensagem que vem após o comando.

Você pode ver como o analisador funciona na classe de teste .

Voa separadamente, costeletas separadamente


Agora precisamos ensinar nosso bot a receber separadamente mensagens, processar e enviar respostas. Após uma série de tentativas e erros, cheguei a essa lógica do aplicativo.
A classe principal Bot funcionará no encadeamento principal do aplicativo e só estará ocupada com o fato de colocar todas as mensagens recebidas em uma fila especial e também será um contêiner para as mensagens que planejamos enviar ao usuário em resposta.

As mudanças nesta classe são muito pequenas. Adicionamos duas filas:

 public final Queue<Object> sendQueue = new ConcurrentLinkedQueue<>(); public final Queue<Object> receiveQueue = new ConcurrentLinkedQueue<>(); 

e reescreveu levemente o código de função public void onUpdateReceived (atualização de atualização)

 @Override public void onUpdateReceived(Update update) { log.debug("Receive new Update. updateID: " + update.getUpdateId()); receiveQueue.add(update); } 

Porque Mais uma vez tentei opções diferentes. E o principal problema do multithreading é trabalhar com dados compartilhados. E acima de tudo, gostei de como a implementação de filas multiencadeadas ConcurrentLinkedQueue <> () lida com isso.
E como você pode ver, nas duas filas, armazenaremos os tipos de dados do objeto. Este é outro marcador para o futuro. Portanto, não estamos apegados aos tipos de mensagens recebidas. Na fila de entrada, podemos adicionar não apenas objetos do tipo Atualização, mas também alguns outros objetos que precisamos.

A mesma coisa com a fila para envio. Como podemos enviar vários tipos de mensagens e eles não têm um pai comum - também usamos um tipo de dados comum - Object.
Se você executar o bot neste formulário, ele funcionará, mas não fará nada. Ele gravará todas as mensagens recebidas no log e colocará na fila.
Portanto, precisamos de algum tipo de encadeamento que receba as mensagens recebidas da fila e execute algumas ações nelas e coloque a fila sendQueue nos resultados de seu trabalho.

Vamos criar um pacote separado: service e nele teremos apenas 2 classes:

MessageReciever - manipulador de mensagens recebidas
MessageSender é o manipulador da fila de mensagens a ser enviado ao usuário.

Consideraremos o trabalho deles um pouco menor, mas, por enquanto, descreveremos seu uso em nosso aplicativo de classe inicial

Depois que nosso bot se conectar, iniciamos nossos manipuladores em threads separados:

 MessageReciever messageReciever = new MessageReciever(test_habr_bot); MessageSender messageSender = new MessageSender(test_habr_bot); test_habr_bot.botConnect(); Thread receiver = new Thread(messageReciever); receiver.setDaemon(true); receiver.setName("MsgReciever"); receiver.setPriority(PRIORITY_FOR_RECEIVER); receiver.start(); Thread sender = new Thread(messageSender); sender.setDaemon(true); sender.setName("MsgSender"); sender.setPriority(PRIORITY_FOR_SENDER); sender.start(); 

Para ambos os threads, especificamos o modo Daemon. Isso é necessário para que os encadeamentos funcionem enquanto o encadeamento principal estiver em execução e se encerrem assim que interromper seu trabalho.

Não gostaríamos de lidar primeiro com o manipulador de mensagens recebidas - vejamos a operação da classe MessageSender .

Vamos dar uma olhada no que ele pode fazer e no que faz:

  • Naturalmente, essa é uma herança da interface para multithreading:
    implementa executável
    e implementação da função de execução

     @Override public void run() 

    Aqui começamos o loop infinito, que só está ocupado com o fato de que ele verifica a fila de envio e chama o comando send

     private void send(Object object) 

    se algo aparecer na fila.
  • No construtor da classe, passamos o objeto da classe Bot, porque a partir dele, pegaremos objetos para enviar mensagens e com ele os enviaremos.
  • O método send determina o tipo de mensagem a ser enviada e aplica o comando apropriado a ela.

Bem, agora vamos ver o trabalho da classe MessageReciever

Ele, como o MessageSender, deve ser multithread, no construtor recebe um objeto da classe Bot, no qual ele recebe as mensagens recebidas em um loop infinito, processa-as e coloca-o na fila para enviar os resultados de seu trabalho.

Aqui usamos o analisador de comandos criado anteriormente. E então adicionamos a capacidade de usar vários tipos de manipuladores para nossas equipes e alguns deles tornaremos multiencadeados.

O ciclo de trabalho é muito simples:

 @Override public void run() { log.info("[STARTED] MsgReciever. Bot class: " + bot); while (true) { for (Object object = bot.receiveQueue.poll(); object != null; object = bot.receiveQueue.poll()) { log.debug("New object for analyze in queue " + object.toString()); analyze(object); } try { Thread.sleep(WAIT_FOR_NEW_MESSAGE_DELAY); } catch (InterruptedException e) { log.error("Catch interrupt. Exit", e); return; } } } 

Verifique a fila. Se houver algo, execute o analisador:

 private void analyze(Object object) 

Se não houver nada, estamos esperando.

O analisador verifica o tipo de objeto. Se ele souber trabalhar com ele, ele inicia o próximo analisador. Se você não pode - jura :)

Porque Novamente, este é um marcador para o futuro e, espero, divulgarei nas próximas partes desta série de artigos. Essa implementação nos permitirá formar nossas próprias tarefas para o bot, criar listas de discussão, tarefas diárias. Para isso, o receptor deve ser capaz de processar não apenas objetos do tipo Update, mas também algo nosso. Mas mais sobre isso mais tarde :)

Vamos considerar o analisador para o tipo de atualização em mais detalhes:

 private void analyzeForUpdateType(Update update) { Long chatId = update.getMessage().getChatId(); String inputText = update.getMessage().getText(); ParsedCommand parsedCommand = parser.getParsedCommand(inputText); AbstractHandler handlerForCommand = getHandlerForCommand(parsedCommand.getCommand()); String operationResult = handlerForCommand.operate(chatId.toString(), parsedCommand, update); if (!"".equals(operationResult)) { SendMessage message = new SendMessage(); message.setChatId(chatId); message.setText(operationResult); bot.sendQueue.add(message); } } 

Ele define o ID do bate-papo. Obtém o texto da mensagem. Usando o analisador, ele determina se a mensagem é um comando e determina qual manipulador esse comando deve ser processado. Ele inicia o processamento do comando e, se o processamento retornar algum texto não vazio - ele forma uma mensagem para enviar ao usuário e a coloca na fila.

E então você deve ter uma pergunta: "Que tipo de manipulador?". Não houve conversa sobre ele antes, e ele não foi mencionado no código. Tudo bem. Agora vamos analisar essa funcionalidade.

Para fazer isso, crie um pacote separado, que armazenará todos os nossos manipuladores. Chame de manipulador
Vamos criar uma classe abstrata AbstractHandler

 import com.example.telegrambot.bot.Bot; import com.example.telegrambot.command.ParsedCommand; import org.telegram.telegrambots.api.objects.Update; public abstract class AbstractHandler { Bot bot; AbstractHandler(Bot bot) { this.bot = bot; } public abstract String operate(String chatId, ParsedCommand parsedCommand, Update update); } 

Ele terá um construtor básico no qual passamos com qual objeto Bot ele precisará interagir. E é declarada uma função abstrata para operar , cuja implementação teremos que registrar nos herdeiros de nossa classe.

Imediatamente, implementaremos o manipulador mais simples que não fará nada e o usaremos quando não entendermos o tipo de comando que recebemos e nenhuma resposta for necessária do bot.

DefaultHandler.java

 import com.example.telegrambot.bot.Bot; import com.example.telegrambot.command.ParsedCommand; import org.apache.log4j.Logger; import org.telegram.telegrambots.api.objects.Update; public class DefaultHandler extends AbstractHandler { private static final Logger log = Logger.getLogger(DefaultHandler.class); public DefaultHandler(Bot bot) { super(bot); } @Override public String operate(String chatId, ParsedCommand parsedCommand, Update update) { return ""; } } 

Como vamos aplicá-lo e onde obteremos os resultados de seu trabalho - analisaremos um pouco mais tarde.

O próximo na fila é o SystemHandler
Ele lidará com comandos básicos, como start, help, e também o instruiremos a executar o comando id

A base disso é assim:

 import com.example.telegrambot.bot.Bot; import com.example.telegrambot.command.Command; import com.example.telegrambot.command.ParsedCommand; import org.apache.log4j.Logger; import org.telegram.telegrambots.api.methods.send.SendMessage; import org.telegram.telegrambots.api.objects.Update; public class SystemHandler extends AbstractHandler { private static final Logger log = Logger.getLogger(SystemHandler.class); private final String END_LINE = "\n"; public SystemHandler(Bot bot) { super(bot); } @Override public String operate(String chatId, ParsedCommand parsedCommand, Update update) { Command command = parsedCommand.getCommand(); switch (command) { case START: bot.sendQueue.add(getMessageStart(chatId)); break; case HELP: bot.sendQueue.add(getMessageHelp(chatId)); break; case ID: return "Your telegramID: " + update.getMessage().getFrom().getId(); } return ""; } 

Você pode ver como a resposta ao comando start e help é formada no código :)
Formamos mensagens de texto e as colocamos em uma fila para envio. Sobre isso, o trabalho do manipulador para. Quem e como enviará essas mensagens - ele não se importa.
E lembre-se, mencionei um pouco acima que, como resultado do manipulador, ele retorna alguns dados de texto. E se esta linha não estiver vazia, devemos enviar este texto ao usuário. Esta é exatamente a funcionalidade que usamos ao elaborar o comando ID:

 case ID: return "Your telegramID: " + update.getMessage().getFrom().getId(); 

O manipulador retornará o texto com o ID do usuário para a pessoa que o chamou e já será gerada uma mensagem para envio, que será encaminhada para a fila.

E no começo do artigo, mencionei que estamos implementando essa opção para processar uma mensagem de um usuário que precisa de tempo para trabalhar. E para que não interfira com nossos manipuladores, alocá-lo-emos em um fluxo separado e deixaremos seus negócios sem distrair o resto.
Como um segmento "pesado", criei o comando de notificação. O princípio de seu trabalho é esse.

Enviando ao bot um comando como:
/ notificar 300

O bot deve informar que a equipe entendeu e, após 300 segundos, enviará uma notificação de que 300 segundos se passaram. Essa equipe pode até ter um uso prático :)

Por exemplo, você coloca bolinhos no fogo e precisa removê-los após 5 minutos. O bot fará isso perfeitamente e emitirá uma notificação no chat de que o tempo acabou.

Ou faça uma tarefa mais séria. Você vai a uma reunião importante e sabe que, ao se comunicar com alguém, precisará interromper a conversa. Para isso, eles geralmente pedem aos amigos que liguem ou escrevam uma mensagem, o que será um motivo para não distrair a conversa por um longo tempo e tomar alguma ação. Mas por que incomodar os amigos quando você tem um bot? Depois de solicitar uma tarefa com antecedência e indicar a hora, você receberá a notificação necessária em telegramas. Mas esta é toda a letra. A tarefa e este comando foram inventados apenas para mostrar como alocar em um fluxo separado algo cujo trabalho pode levar um período muito longo.

Portanto, NotifyHandler :

 import com.example.telegrambot.ability.Notify; import com.example.telegrambot.bot.Bot; import com.example.telegrambot.command.ParsedCommand; import org.apache.log4j.Logger; import org.telegram.telegrambots.api.objects.Update; public class NotifyHandler extends AbstractHandler { private static final Logger log = Logger.getLogger(NotifyHandler.class); private final int MILLISEC_IN_SEC = 1000; private String WRONG_INPUT_MESSAGE = "Wrong input. Time must be specified as an integer greater than 0"; public NotifyHandler(Bot bot) { super(bot); } @Override public String operate(String chatId, ParsedCommand parsedCommand, Update update) { String text = parsedCommand.getText(); if ("".equals(text)) return "You must specify the delay time. Like this:\n" + "/notify 30"; long timeInSec; try { timeInSec = Long.parseLong(text.trim()); } catch (NumberFormatException e) { return WRONG_INPUT_MESSAGE; } if (timeInSec > 0) { Thread thread = new Thread(new Notify(bot, chatId, timeInSec * MILLISEC_IN_SEC)); thread.start(); } else return WRONG_INPUT_MESSAGE; return ""; } } 

Verificamos se o tempo de atraso nos foi dado no texto. Se não, nós juramos. Nesse caso, iniciamos um novo segmento, onde passamos o introdutório em nossas instruções. Esta tarefa será tratada por uma classe Notify separada.
A funcionalidade é extremamente simples. Ele dorme o número indicado de segundos. Mas, durante o sono dele, seu bot pode receber outras mensagens, se comunicar com você e lançar notificações adicionais adicionais. E tudo isso funciona separadamente um do outro.

E para concluir logicamente todo esse grupo com manipuladores de chamada, vamos voltar à nossa classe MessageReciever e ver como entendemos qual manipulador precisamos e como os executamos.
O manipulador necessário nos é retornado pelo comando
 private AbstractHandler getHandlerForCommand(Command command) { if (command == null) { log.warn("Null command accepted. This is not good scenario."); return new DefaultHandler(bot); } switch (command) { case START: case HELP: case ID: SystemHandler systemHandler = new SystemHandler(bot); log.info("Handler for command[" + command.toString() + "] is: " + systemHandler); return systemHandler; case NOTIFY: NotifyHandler notifyHandler = new NotifyHandler(bot); log.info("Handler for command[" + command.toString() + "] is: " + notifyHandler); return notifyHandler; default: log.info("Handler for command[" + command.toString() + "] not Set. Return DefaultHandler"); return new DefaultHandler(bot); } } 

Agora, se você quiser adicionar mais alguns comandos, precisará fazer o seguinte:

  1. Na classe Command, adicione a sintaxe do comando.
  2. No receptor, na função getHandlerForCommand, especifique quem será o responsável por processar este comando.
  3. E realmente escreva esse manipulador.

Eu direi antecipadamente que o processo de adição de novas equipes pode ser simplificado. Manipuladores responsáveis ​​podem ser registrados imediatamente na classe com uma lista de comandos. Mas receio que o código não seja fácil de entender. O texto é muito longo. Mas não posso vencê-lo em pedaços. Três funções básicas de bot são descritas aqui, que funcionam apenas juntas e não é correto falar sobre elas individualmente.

Sobre o que falaremos nas seguintes partes?

Precisamos entender como formar diferentes tipos de mensagens. Como trabalhar com o teclado e os botões. Como editar suas postagens antigas. Como trabalhar com retornos de chamada. Como atribuir tarefas ao bot para executar algumas ações. Como criar uma mensagem interativa com um bot e muito mais. Todas as outras partes dependem de você e de sua atividade.
Nos comentários, aguardo seus comentários e orientações, que consideraremos como uma prioridade.

Sinta-se livre para fazer perguntas. Se algo não estiver indicado no artigo ou em algum momento não estiver claro - escreva-me sobre isso. Definitivamente vou corrigir, editar ou esclarecer questões controversas.

Programe com prazer e que a força e o belo código acompanhem você :)

py.s.

O bot escrito nesta parte do artigo funciona. Você pode atormentá-lo aqui: @test_habr_bot
Você também pode atormentar meu planejador: @EventCheckPlanner_Bot
E o fã de cinema atrevido : @FilmFanAmateurBot .

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


All Articles