节点 Telegram 机器人 api,在用户之间进行链式对话
Node Telegram bot api, make chain conversation between user
我已经开始使用这个 module
为电报编写机器人逻辑
我可以像这样创建简单的问答逻辑:
bot.onText(/\/start/, function(msg, match){
bot.sendMessage(msg.chat.id, "Hello this is great bot");
});
当用户键入 /start
时,他将收到此消息。
我想在机器人和用户之间创建类似链式对话的东西。比如当用户输入 /buy
机器人会显示购买选项,在用户输入他想购买的东西后,机器人会显示所选产品的类型等等。
如何在用户和机器人之间创建链式对话?如何让机器人记住以前选择的命令并在需要时重置它们?我需要保留在自己的数据库中才能执行此操作吗?
你可以用不同的方式来做。
- 你可以存储"state"用户在你自己
- 您可以使用多个独立运行的命令,但您只是给用户一种被引导的感觉
- 你可以使用
ForceReply
的Bot API
好的。所以对于 1.
我会说你有一些好处。当用户不处于正确状态时,您实际上可以引导用户并限制对某些命令的访问。假设他想买爆米花,但他在鞋店里,您可以通过检查保存的用户状态来禁止该命令。
对于 2.
,您将始终允许用户使用 /buy
、/buy_popcorn
和 /buy_shoe
。但是根据您的回答,您只需给他特定数量的可能选择。
User: /buy
What do you want to buy? /shoes or /food :Bot
User: /food
How about some Popcorn? Use /buy_popcorn :Bot
User: /buy_shoe
Alright. Shoes added to cart :Bot
这是允许的,但用户必须手动编写 /buy_shoe
3.
可能的方法是使用 ForceReply。用户将自动收到 answer to
消息。因此,当他使用 /buy_shoe
时,他将回复机器人发送的最后一条消息。您还将获得用户在来自 api 的消息中回答的消息。您可以检查用户回答的消息是否是该命令的正确先决条件/正确消息,然后限制或允许该命令。
User: /buy
What do you want to buy? /shoes or /food :Bot
User: [Answer to: What do you...] /food
How about some Popcorn? Use /buy_popcorn :Bot
User: [Answer to: How about some...] /buy_shoe
Sorry, but you're currently in the Food Store :Bot
我想这归结为个人喜好。但所有这些都有利有弊,您必须决定是否要在没有先决条件的情况下允许特定命令。
此列表可能不完整。可能还有其他方法,我没想到。但是这3种是我知道的。
我也有这个问题,我需要我的机器人根据他对用户的最后回答来回答问题,因为很难找到可以引导我找到解决方案的想法(在 Java), 为了未来的 Java 谷歌员工,我将在这里分享我的。我正在使用 telegrambots library 和 Spring Boot/Data.
正如 Loki 指出的那样,实现此流程的最佳方式是在数据库中保存状态。为此,使用消息的唯一聊天 ID 来区分聊天。
这里是 Java 实现的相关部分(逻辑几乎适用于任何语言):
保存与系统用户相关的 Telegram 聊天信息的实体。
@Entity
@Table(name = "user_bot")
public class UserBot implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "chat_id", unique = true, nullable = false, length = 255)
private String chatId;
@Column(name = "bot_verification_code", length = 6)
private String botVerificationCode;
@Enumerated
@Column(name = "last_bot_state", columnDefinition = "SMALLINT DEFAULT NULL")
private BotState lastBotState;
@Column(columnDefinition = "TINYINT(1)")
private boolean verified;
@JoinColumn(name = "user_id", referencedColumnName = "id")
@ManyToOne(fetch = FetchType.EAGER)
private User user;
}
表示所有可能的机器人响应(状态)的枚举。
public enum BotState {
// Respostas do bot que representam estados
AUTH_STEP_1("Muito bem. Qual é o seu e-mail no sistema?"), AUTH_STEP_2("Enviei um código para o seu e-mail. Por favor, digite-o aqui."),
NO_STATE("");
private final String state;
private BotState(String state) {
this.state = state;
}
@Override
public String toString() {
return this.state;
}
}
接收消息并做出相应响应的服务。
@Service
public class TelegramBotService extends TelegramLongPollingBot {
@Autowired
private CodeUtils codeUtils;
@Autowired
private UserBotRepository userBotRepository;
@Autowired
private UserRepository userRepository;
@Value("${telegram.bot.username}")
private String botUsername;
@Value("${telegram.bot.token}")
private String botToken;
@PostConstruct
public void registerBot() {
TelegramBotsApi botsApi = new TelegramBotsApi();
try {
botsApi.registerBot(this);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
@Override
public void onUpdateReceived(Update update) {
if (update.hasMessage() && update.getMessage().hasText()) {
String receivedMessage = update.getMessage().getText();
SendMessage sendMessage = null;
// TODO: futuramente, tratar casos onde um usuário chama um comando sem ainda estar autenticado
switch (receivedMessage) {
case "/autenticar":
sendMessage = handleAuthentication(update);
break;
default:
// Quando nenhum comando atender, será um texto a ser checado de acordo com o estado anterior
sendMessage = checkState(update);
}
try {
execute(sendMessage);
} catch (TelegramApiException e) {
codeUtils.log(e.getMessage(), this);
}
}
}
private SendMessage handleAuthentication(Update update) {
SendMessage sendMessage = new SendMessage()
.setChatId(update.getMessage().getChatId())
.setText(BotState.AUTH_STEP_1.toString());
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
if (userBot == null) {
userBot = new UserBot();
userBot.setChatId(update.getMessage().getChatId().toString());
userBot.setLastBotState(BotState.AUTH_STEP_1);
} else if (userBot.isVerified()) {
// Um texto simples enviado no sendMessage indica o fim de um fluxo
sendMessage.setText("Este aparelho já está autenticado no sistema.");
userBot.setLastBotState(null);
}
userBotRepository.save(userBot);
return sendMessage;
}
// Checa o estado anterior do bot em relação ao chatId recebido
private SendMessage checkState(Update update) {
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
SendMessage sendMessage = null;
if (userBot == null || userBot.getLastBotState() == null)
return sendDefaultMessage(update);
switch (Optional.ofNullable(userBot.getLastBotState()).orElse(BotState.NO_STATE)) {
case AUTH_STEP_1:
sendMessage = sendCode(update);
break;
case AUTH_STEP_2:
sendMessage = validateCode(update);
break;
default:
sendMessage = sendDefaultMessage(update);
}
return sendMessage;
}
// Grava o código no banco e envia para o e-mail do usuário
private SendMessage sendCode(Update update) {
User user = userRepository.findByEmail(update.getMessage().getText().toLowerCase());
SendMessage sendMessage = new SendMessage(update.getMessage().getChatId(), "");
if (user == null)
sendMessage.setText("Não encontrei nenhum usuário no sistema com este e-mail :(");
else {
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
String verificationCode = Integer.toString(new Random().nextInt(899999) + 100000);
String text = "Este é um e-mail automático de verificação de identidade. Informe este código para o bot do Telegram: " + verificationCode;
codeUtils.sendEmail(new String[]{user.getEmail()}, "CCR Laudos - Código de Verificação", text);
// Associa a conversação ao usuário, mas a validade depende da flag verified
userBot.setUser(user);
userBot.setBotVerificationCode(verificationCode);
userBot.setLastBotState(BotState.AUTH_STEP_2);
userBotRepository.save(userBot);
sendMessage.setText(BotState.AUTH_STEP_2.toString());
}
return sendMessage;
}
// Checa se o código informado foi o mesmo passado por e-mail para o usuário a fim de autenticá-lo
private SendMessage validateCode(Update update) {
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
SendMessage sendMessage = new SendMessage(update.getMessage().getChatId(), "");
if (update.getMessage().getText().equals(userBot.getBotVerificationCode())) {
userBot.setVerified(true);
sendMessage.setText("O aparelho foi autenticado com sucesso. Você passará a receber notificações do sistema.");
} else {
userBot.setUser(null);
sendMessage.setText("Código inválido.");
}
userBotRepository.save(userBot);
return sendMessage;
}
private SendMessage sendDefaultMessage(Update update) {
String markdownMessage = "Não entendi \ud83e\udd14 \n"
+ "Que tal tentar um comando digitando */* ?";
return new SendMessage(update.getMessage().getChatId(), markdownMessage).setParseMode(ParseMode.MARKDOWN);
}
@Override
public String getBotUsername() {
return this.botUsername;
}
@Override
public String getBotToken() {
return this.botToken;
}
}
实现的流程是:
1 - 用户发送 /authenticate。
2 - 系统对设备一无所知,因此存储聊天 ID 和最后状态。 最后的状态将是对用户的响应。系统要求提供用户的电子邮件。
3 - 用户发送他的电子邮件。
4 - 文本未被识别为命令,因此系统检查是否存在与此聊天 ID 相关的最后状态。如果存在先前状态,则使用传入文本作为此状态方法的参数。系统向用户的电子邮件发送代码并要求提供。
5 - 用户发送代码。
6 - 如果密码正确,系统会再次检查之前的状态并验证用户。
就是这样!希望对大家有帮助。
我已经开始使用这个 module
为电报编写机器人逻辑我可以像这样创建简单的问答逻辑:
bot.onText(/\/start/, function(msg, match){
bot.sendMessage(msg.chat.id, "Hello this is great bot");
});
当用户键入 /start
时,他将收到此消息。
我想在机器人和用户之间创建类似链式对话的东西。比如当用户输入 /buy
机器人会显示购买选项,在用户输入他想购买的东西后,机器人会显示所选产品的类型等等。
如何在用户和机器人之间创建链式对话?如何让机器人记住以前选择的命令并在需要时重置它们?我需要保留在自己的数据库中才能执行此操作吗?
你可以用不同的方式来做。
- 你可以存储"state"用户在你自己
- 您可以使用多个独立运行的命令,但您只是给用户一种被引导的感觉
- 你可以使用
ForceReply
的Bot API
好的。所以对于 1.
我会说你有一些好处。当用户不处于正确状态时,您实际上可以引导用户并限制对某些命令的访问。假设他想买爆米花,但他在鞋店里,您可以通过检查保存的用户状态来禁止该命令。
对于 2.
,您将始终允许用户使用 /buy
、/buy_popcorn
和 /buy_shoe
。但是根据您的回答,您只需给他特定数量的可能选择。
User: /buy
What do you want to buy? /shoes or /food :Bot
User: /food
How about some Popcorn? Use /buy_popcorn :Bot
User: /buy_shoe
Alright. Shoes added to cart :Bot
这是允许的,但用户必须手动编写 /buy_shoe
3.
可能的方法是使用 ForceReply。用户将自动收到 answer to
消息。因此,当他使用 /buy_shoe
时,他将回复机器人发送的最后一条消息。您还将获得用户在来自 api 的消息中回答的消息。您可以检查用户回答的消息是否是该命令的正确先决条件/正确消息,然后限制或允许该命令。
User: /buy
What do you want to buy? /shoes or /food :Bot
User: [Answer to: What do you...] /food
How about some Popcorn? Use /buy_popcorn :Bot
User: [Answer to: How about some...] /buy_shoe
Sorry, but you're currently in the Food Store :Bot
我想这归结为个人喜好。但所有这些都有利有弊,您必须决定是否要在没有先决条件的情况下允许特定命令。
此列表可能不完整。可能还有其他方法,我没想到。但是这3种是我知道的。
我也有这个问题,我需要我的机器人根据他对用户的最后回答来回答问题,因为很难找到可以引导我找到解决方案的想法(在 Java), 为了未来的 Java 谷歌员工,我将在这里分享我的。我正在使用 telegrambots library 和 Spring Boot/Data.
正如 Loki 指出的那样,实现此流程的最佳方式是在数据库中保存状态。为此,使用消息的唯一聊天 ID 来区分聊天。
这里是 Java 实现的相关部分(逻辑几乎适用于任何语言):
保存与系统用户相关的 Telegram 聊天信息的实体。
@Entity
@Table(name = "user_bot")
public class UserBot implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "chat_id", unique = true, nullable = false, length = 255)
private String chatId;
@Column(name = "bot_verification_code", length = 6)
private String botVerificationCode;
@Enumerated
@Column(name = "last_bot_state", columnDefinition = "SMALLINT DEFAULT NULL")
private BotState lastBotState;
@Column(columnDefinition = "TINYINT(1)")
private boolean verified;
@JoinColumn(name = "user_id", referencedColumnName = "id")
@ManyToOne(fetch = FetchType.EAGER)
private User user;
}
表示所有可能的机器人响应(状态)的枚举。
public enum BotState {
// Respostas do bot que representam estados
AUTH_STEP_1("Muito bem. Qual é o seu e-mail no sistema?"), AUTH_STEP_2("Enviei um código para o seu e-mail. Por favor, digite-o aqui."),
NO_STATE("");
private final String state;
private BotState(String state) {
this.state = state;
}
@Override
public String toString() {
return this.state;
}
}
接收消息并做出相应响应的服务。
@Service
public class TelegramBotService extends TelegramLongPollingBot {
@Autowired
private CodeUtils codeUtils;
@Autowired
private UserBotRepository userBotRepository;
@Autowired
private UserRepository userRepository;
@Value("${telegram.bot.username}")
private String botUsername;
@Value("${telegram.bot.token}")
private String botToken;
@PostConstruct
public void registerBot() {
TelegramBotsApi botsApi = new TelegramBotsApi();
try {
botsApi.registerBot(this);
} catch (TelegramApiException e) {
e.printStackTrace();
}
}
@Override
public void onUpdateReceived(Update update) {
if (update.hasMessage() && update.getMessage().hasText()) {
String receivedMessage = update.getMessage().getText();
SendMessage sendMessage = null;
// TODO: futuramente, tratar casos onde um usuário chama um comando sem ainda estar autenticado
switch (receivedMessage) {
case "/autenticar":
sendMessage = handleAuthentication(update);
break;
default:
// Quando nenhum comando atender, será um texto a ser checado de acordo com o estado anterior
sendMessage = checkState(update);
}
try {
execute(sendMessage);
} catch (TelegramApiException e) {
codeUtils.log(e.getMessage(), this);
}
}
}
private SendMessage handleAuthentication(Update update) {
SendMessage sendMessage = new SendMessage()
.setChatId(update.getMessage().getChatId())
.setText(BotState.AUTH_STEP_1.toString());
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
if (userBot == null) {
userBot = new UserBot();
userBot.setChatId(update.getMessage().getChatId().toString());
userBot.setLastBotState(BotState.AUTH_STEP_1);
} else if (userBot.isVerified()) {
// Um texto simples enviado no sendMessage indica o fim de um fluxo
sendMessage.setText("Este aparelho já está autenticado no sistema.");
userBot.setLastBotState(null);
}
userBotRepository.save(userBot);
return sendMessage;
}
// Checa o estado anterior do bot em relação ao chatId recebido
private SendMessage checkState(Update update) {
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
SendMessage sendMessage = null;
if (userBot == null || userBot.getLastBotState() == null)
return sendDefaultMessage(update);
switch (Optional.ofNullable(userBot.getLastBotState()).orElse(BotState.NO_STATE)) {
case AUTH_STEP_1:
sendMessage = sendCode(update);
break;
case AUTH_STEP_2:
sendMessage = validateCode(update);
break;
default:
sendMessage = sendDefaultMessage(update);
}
return sendMessage;
}
// Grava o código no banco e envia para o e-mail do usuário
private SendMessage sendCode(Update update) {
User user = userRepository.findByEmail(update.getMessage().getText().toLowerCase());
SendMessage sendMessage = new SendMessage(update.getMessage().getChatId(), "");
if (user == null)
sendMessage.setText("Não encontrei nenhum usuário no sistema com este e-mail :(");
else {
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
String verificationCode = Integer.toString(new Random().nextInt(899999) + 100000);
String text = "Este é um e-mail automático de verificação de identidade. Informe este código para o bot do Telegram: " + verificationCode;
codeUtils.sendEmail(new String[]{user.getEmail()}, "CCR Laudos - Código de Verificação", text);
// Associa a conversação ao usuário, mas a validade depende da flag verified
userBot.setUser(user);
userBot.setBotVerificationCode(verificationCode);
userBot.setLastBotState(BotState.AUTH_STEP_2);
userBotRepository.save(userBot);
sendMessage.setText(BotState.AUTH_STEP_2.toString());
}
return sendMessage;
}
// Checa se o código informado foi o mesmo passado por e-mail para o usuário a fim de autenticá-lo
private SendMessage validateCode(Update update) {
UserBot userBot = userBotRepository.findByChatId(update.getMessage().getChatId().toString());
SendMessage sendMessage = new SendMessage(update.getMessage().getChatId(), "");
if (update.getMessage().getText().equals(userBot.getBotVerificationCode())) {
userBot.setVerified(true);
sendMessage.setText("O aparelho foi autenticado com sucesso. Você passará a receber notificações do sistema.");
} else {
userBot.setUser(null);
sendMessage.setText("Código inválido.");
}
userBotRepository.save(userBot);
return sendMessage;
}
private SendMessage sendDefaultMessage(Update update) {
String markdownMessage = "Não entendi \ud83e\udd14 \n"
+ "Que tal tentar um comando digitando */* ?";
return new SendMessage(update.getMessage().getChatId(), markdownMessage).setParseMode(ParseMode.MARKDOWN);
}
@Override
public String getBotUsername() {
return this.botUsername;
}
@Override
public String getBotToken() {
return this.botToken;
}
}
实现的流程是:
1 - 用户发送 /authenticate。
2 - 系统对设备一无所知,因此存储聊天 ID 和最后状态。 最后的状态将是对用户的响应。系统要求提供用户的电子邮件。
3 - 用户发送他的电子邮件。
4 - 文本未被识别为命令,因此系统检查是否存在与此聊天 ID 相关的最后状态。如果存在先前状态,则使用传入文本作为此状态方法的参数。系统向用户的电子邮件发送代码并要求提供。
5 - 用户发送代码。
6 - 如果密码正确,系统会再次检查之前的状态并验证用户。
就是这样!希望对大家有帮助。