Telegram Bot - Process user response based on last bot question
Asked Answered
K

3

9

I want my Telegram bot to process user inputs based on the last question the bot asked. Basically, this is the flow:

  • User calls /authenticate command
  • Bot asks for email
  • User sends his email
  • Bot answers with a message about sending a code to the user's email for confirmation and asks for the user to type the code on the chat
  • User types the code
  • Bot validates the user code and user is authenticated and start receiving notifications

The problem is: how do i know that the user is answering a specific bot question in this flow?

I thought about two ways to do so:

  • Send the message with force reply option so the user has to reply to the bot question. This would send me back the message to which the user is responding so i could compare the bot message string to see what was the answer about.

  • Store the last bot message somewhere, then when a message arrives, check what was the last bot message and assume that the user message is a response.

Is there a better way? I am using Java with telegrambots library.

Knighten answered 15/12, 2019 at 3:59 Comment(0)
K
6

Since it was quite hard to find ideas that could lead me to a solution (in Java), I am going to share mine here for the sake of future Java googlers. I am using telegrambots library together with Spring Boot/Data.

The best way to implement this flow is saving states on your database. To do so, use the messages unique chat ids to distinguish a chat from another.

Here is the relevant part of the Java implementation (the logic pretty much applies to any language):

The entity that holds Telegram chat information related to a system user.

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

The enum that represents all possible bot responses (states).

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

The service that receives messages and respond accordingly.

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

The implemented flow is:

  1. User sends /authenticate.

  2. System knows nothing about the device, so store the chat id and the last state. The last state will be the response to the user. System asks for the user's e-mail.

  3. User sends his e-mail.

  4. The text is not recognized as a command, so system checks if there is a last state relative to this chat id. If a previous state exists, use the incoming text as a parameter to this state's method. System sends a code to the user's e-mail and asks for it.

  5. User sends the code.

  6. System checks the previous state again and authenticates the user, if the code is correct.

That's it! Hope it helps someone.

Knighten answered 2/11, 2020 at 5:51 Comment(0)
G
0

Usually to be specific on what are you sending to the telegram bot, you would communicate with the bot using commands, (telegram commands starts with /, and each command have a specific command handler on the server side), unfortunately, so far there is no way to send extra params along with the command to the telegram bot, you can use one of the workarounds mentioned in the conversation below : How do I have my Bot respond with arguments?

if you are not familiar with bot commands using java, please refer to the following examples: https://www.programcreek.com/java-api-examples/?api=org.telegram.telegrambots.bots.AbsSender

I hope this helps.

Gensler answered 15/12, 2019 at 6:19 Comment(1)
Yes, it is not possible to use arguments when you send a command. But what i would like to do is imitate a flow like BotFather does when you create a bot.Knighten
F
0

One of the most popular tasks when developing telegram bots.
There is a solution, you need to install the shapoapps/multibot_driver package (composer require shapoapps/multibot_driver) for Laravel.
This package has a session manager, an analogue of sessions for web page users. After each user message, you record user input into the session on server side. When each new request(message) received, you read the data from the session and build the logic.
Here's the documentation - https://github.com/shapoapps/multibot_driver

Feedback answered 16/12, 2019 at 16:51 Comment(1)
I am using Java :(Knighten

© 2022 - 2024 — McMap. All rights reserved.