TelegramBot。 基本功能。 分开飞,肉饼分开。 (第二部分)

我们将继续以电报形式开发该机器人的基本功能。 在前面的部分中,我们讨论了机器人在接收消息,处理和发送方面的工作应分开的观点。 让我们尝试使用基本的Java Core工具来使我们的机器人成为多线程和异步的。 我们将提出一项需要大量时间来处理的任务。 让我们看看电报中的命令如何工作以及如何处理它们。

这是关于Java电报编程机器人的文章的第一部分的继续
TelegramBot说明,用于为机器人创建基本功能。 (第1部分)
进一步有趣的是,欢迎您...

我必须马上说,在这部分中,所有内容都被一次性添加了,我们将静静地分析允许该机器人能够进行多线程的所有功能,以及为什么需要它。

像往常一样主要:
您可以在git信息库的Part2-Handlers分支中找到本文的所有现成代码。
该代码可以正常工作,足以倾斜,更改用于授权机器人的数据(名称和令牌)并运行App.class类中的main方法。

请注意,当机器人启动时,此类会向机器人管理员发送通知,说明该机器人已启动。 机器人管理员ID也已在App.class类中指定,如果不进行更改,您的机器人将尝试向我发送消息:)

接下来,我们将分析第一部分发布后出现的那些变化。

命令处理


首先,让我们处理一下与电报机器人进行通信的系统中团队通常的概念。 根据僵尸程序的设置,它可以查看任何格式的消息,也可以仅查看专门设计的命令。 有什么区别和
在这里您可以满足这些消息选项。

  1. 纯文本,常规消息。
    通过这种形式,机器人在PM中向他发送邮件时会收到消息。 但是,如果在漫游器的设置关闭了分组隐私模式 ,则漫游器将开始完全查看所有消息。 如果启用此设置,则在添加到组中后,漫游器只会看到发给该组的命令。 外观-请参阅第二段
  2. 专门设计的团队
    这样的命令总是以斜杠开头: /
    之后是团队本身。 命令文本不应包含空格。 一个例子:
    /开始
    使用此命令,任何用户总是可以开始与您的机器人进行通信。 因此,根据良好形式的规则,必须规定对此命令的响应。

    机器人知道如何使用的所有命令,建议将其添加到机器人设置的技能列表中。 所有这些都通过@BotFather电报完成。

    通过调用/ myBots命令,选择您的机器人,然后选择“编辑机器人”按钮
    您将看到一个窗口,其中将显示该机器人的所有参数,然后您可以配置其整个界面并指出您的机器人可以使用哪些命令。



    它们设置为以下格式:



    然后,当您开始向机器人输入命令时,它将显示列出的命令列表的帮助:



    还有一点细微差别。 一个组可能包含多个bot,并且如果它们具有通用命令(并且通用命令是强制性的,大多数bot都实现了相同的启动和帮助),则一部分将被添加到命令本身,告诉该命令属于哪个bot。 该命令将看起来完全像这样:
    /开始@ test_habr_bot

现在,了解所有这些细微差别,让我们为您创建一个处理选项,该选项应理解以斜杠开头的命令,并知道如何区分该命令是专门针对您的机器人还是其他机器人。

创建一个包,其中包含负责处理命令的类。
包com.example.telegrambot.command

在Command类中,我们列出了bot应该能够理解的所有命令。

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

如您先前所见,我在@BotFather指出我应该能够理解4个团队的机器人。 这些将是标准的启动和帮助。 我们添加一个有用的-id。 再通知一遍,稍后我会谈到。 还有两个团队NONE和NOTFORME,它们将告诉我们文本消息根本不是命令,或者该命令不适用于我们的机器人。

添加另一个帮助器类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=""; } 

其主要目的是将解析文本的结果存储在此类的对象中。 它仅包含团队本身以及团队之后的所有文本。

我们将编写一个单独的类,将这些团队解析给我们。 解析器

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

简而言之。 初始化解析器时,我们必须在构造函数中传递bot的名称,以便解析器可以将其命令与陌生人区分开。

好吧,那我们就调用public方法

 public ParsedCommand getParsedCommand(String text) 

我们将参数文本中的消息文本传递给该消息文本,他必须向我们返回命令,并且在命令之后出现消息文本。

您可以在测试类中看到解析器的工作方式。

分开飞,炸肉排


现在我们需要教我们的机器人分别接收消息,处理和发送响应。 经过一系列的试验和错误,我来到了这个应用程序的逻辑。
Bot主类将在应用程序的主线程中工作,并且只会忙于将所有接收到的消息放入特殊队列的事实,并且还将成为我们计划发送给用户作为响应的消息的容器。

此类中的更改很小。 我们添加了两个队列:

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

并稍微重写了功能代码public void onUpdateReceived(更新)

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

为什么这样 我再次尝试了不同的选择。 多线程的主要问题是使用共享数据。 最重要的是,我最喜欢多线程队列ConcurrentLinkedQueue <>()的实现方式
如您所见,在这两个队列中,我们将存储对象数据类型。 这是未来的又一书签。 因此,我们并不局限于收到的消息类型。 在传入队列中,我们不仅可以添加Update类型的对象,还可以添加我们需要的其他一些对象。

与发送队列相同。 由于我们可以发送各种类型的消息,并且它们没有共同的父对象-我们也使用共同的数据类型-对象。
如果您以这种形式运行机器人,它将起作用,但不会执行任何操作。 他会将所有收到的消息记录在日志中并放入队列中。
因此,我们需要某种线程来接收从队列接收的消息并对其执行一些操作,并将sendQueue队列放入其工作结果中。

让我们创建一个单独的包: service ,其中只有2个类:

MessageReciever-收到的消息处理程序
MessageSender是要发送给用户的消息队列处理程序。

我们将考虑他们的工作,但是现在,我们将在我们的入门类App中描述他们的使用。

机器人连接后,我们在单独的线程中启动处理程序:

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

对于两个线程,我们都指定守护程序模式。 为了使线程能够在主线程运行时正常工作,并在其停止工作后立即终止自身,这是必要的。

我们不想首先处理传入的消息处理程序-让我们看一下MessageSender类的操作。

让我们来看看他能做什么和他做什么:

  • 自然,这是多线程接口的继承:
    实现可运行
    功能的设计与实现

     @Override public void run() 

    在这里,我们开始无限循环,它只是忙于以下事实:它检查了发送队列并调用了send命令。

     private void send(Object object) 

    如果队列中出现某些内容。
  • 在类构造函数中,我们传递Bot类的对象,因为 从中我们将获取用于发送消息的对象,并随之发送它们。
  • send方法确定要发送的消息的类型,并对其应用适当的命令。

好了,现在让我们看一下MessageReciever工作

与MessageSender一样,他必须是多线程的,在构造函数中接收Bot类的对象,在该对象中,他将无限循环地接收接收到的消息,对其进行处理,然后将其放入发送工作结果的队列中。

在这里,我们使用先前创建的命令解析器。 然后,我们增加了为团队使用各种类型的处理程序的功能,其中一些将使我们成为多线程的。

工作周期非常简单:

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

检查队列。 如果有东西,请运行分析仪:

 private void analyze(Object object) 

如果什么都没有,我们正在等待。

分析器检查对象的类型。 如果他知道如何与他合作,他将启动下一个分析仪。 如果不能-发誓:)

为什么这样 同样,这是将来的书签,我希望我将在本系列文章的后续部分中进行介绍。 这样的实现将使我们能够为机器人形成我们自己的任务,创建邮件列表和日间任务。 为此,接收器不仅必须能够处理Update类型的对象,而且还必须能够处理我们类型的对象。 但是稍后会更多:)

让我们更详细地考虑更新类型的分析器:

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

它定义了聊天ID。 获取消息文本。 使用解析器,它确定消息是否为命令,并确定应处理该命令的处理程序。 它开始处理命令,并且如果命令的处理返回一些非空文本-它形成一条消息发送给用户并将其放入队列中。

然后,您应该提出一个问题:“哪种处理程序?”。 之前没有谈论过他,代码中也没有提到他。 好吧 现在,我们将分析此功能。

为此,请创建一个单独的包,其中将存储我们的所有处理程序。 称为处理程序
让我们创建一个抽象类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); } 

他将具有一个基本的构造函数,我们将在其中传递他需要与之交互的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 ""; } } 

我们将如何应用它以及在何处获得其工作结果-我们将在稍后进行分析。

接下来的是SystemHandler
他将处理基本命令,例如启动,帮助,我们还将指示他执行id命令。

它的基础看起来像这样:

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

您可以看到在代码中如何形成对start and help命令的响应:)
我们形成文本消息并将其放入发送队列中。 在此,处理程序的工作停止。 谁以及如何发送这些消息-他根本不在乎。
记住,我在上面提到了一点,作为处理程序的结果,它返回一些文本数据。 如果此行不为空,则必须将此文本发送给用户。 这正是我们制定ID命令时使用的功能:

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

处理程序会将带有用户ID的文本返回给调用它的人,并且已经生成了要发送的消息,该消息随后将进入队列。

在本文的开头,我提到我们正在实现此选项,用于处理需要时间工作的用户的消息。 为了不干扰我们的处理程序,我们将在一个单独的流中分配它,并在不分散其余部分的情况下继续其业务。
作为这样的“重量级”线程,我想出了notify命令。 她的工作原理是这样的。

通过向机器人发送以下形式的命令:
/通知300

该漫游器应通知您该团队已理解,并在300秒后会向您发送300秒过去的通知。 这个团队甚至有实际用途:)

例如,您将饺子放在火上,需要在5分钟后将其移除。 机器人将完美地做到这一点,并会在聊天中向您发出通知,通知您时间已到。

或者承担更严重的任务。 您参加重要的会议,并且知道与某人交流时,您需要打断对话。 为此,他们通常要求朋友打电话或写消息,这是不长时间分散注意力并采取行动的动机。 但是,当您拥有机器人时,为什么还要打扰朋友呢? 在事先向他询问任务并指出时间后,您将以电报形式收到必要的通知。 但这就是所有歌词。 该任务和此命令的发明仅是为了向您展示如何在单独的流中分配一些工作可能需要很长时间的事情。

因此, 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 ""; } } 

我们检查文本中是否给了延迟时间。 如果没有,我们发誓。 如果是这样,我们将启动一个新线程,并在其中传递指令说明。 该任务将由单独的Notify类处理。
功能非常简单。 他睡了指定的秒数。 但是在他睡觉的过程中,您的机器人可以接收任何其他消息,与您进行通信并启动其他通知。 所有这一切都彼此分开。

为了在逻辑上完成与调用处理程序的连接,让我们回到MessageReciever类,看看如何了解我们需要哪个处理程序以及如何运行它们。
命令将必要的处理程序返回给我们
 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); } } 

现在,如果要添加更多命令,则需要执行以下操作:

  1. Command类中添加命令语法。
  2. 接收方getHandlerForCommand函数中指定谁负责处理此命令。
  3. 并实际编写此处理程序。

我会事先说,可以简化添加新团队的过程。 负责处理程序可以使用命令列表立即在类中注册。 但是,恐怕代码不容易理解。 文字很长。 但我无法击败他。 这里描述了三个基本的bot功能,这些功能只能一起使用,因此不宜单独讨论它们。

在以下部分中我们将谈论什么?

我们需要了解如何形成不同类型的消息。 如何使用键盘和按钮。 如何编辑您的旧帖子。 如何使用回调。 如何向机器人赋予任务以执行一些操作。 如何使用机器人等创建交互式消息。 所有其他部分都取决于您和您的活动。
在评论中,我期待您的反馈和指导,我们将其视为优先事项。

随时提出问题。 如果文章中未注明或在某些时候不清楚,请给我写信。 我一定会更正,编辑或澄清有争议的问题。

编程愉快,并可能伴随着力量和优美的代码:)

py.s.

在本文的这一部分中编写的机器人起作用。 您可以在这里折磨他: @test_habr_bot
您也可以折磨我的计划者: @EventCheckPlanner_Bot
还有厚脸皮的电影迷@FilmFanAmateurBot

Source: https://habr.com/ru/post/zh-CN481354/


All Articles