skip to Main Content

I have started to write bot logic for telegram by using this module

I can create simple question and answer logic like this:

bot.onText(//start/, function(msg, match){
  bot.sendMessage(msg.chat.id, "Hello this is great bot");
});

When user types /start he will receive this message.

I want to create something like chained conversation between bot and user. Like when user types /buy bot will show options to buy, after that user types what he wants to buy then bot will show types of selected product and so on.

How is it possible to create chained conversation between user and bot? How to make bot remember previous selected commands and reset them when it is time? Do I need to keep in my own database in order to do that?

2

Answers


  1. You can do it in different ways.

    1. You can store the “state” the user is in yourself
    2. You can use multiple commands that will work on their own, but you just give the user the feeling of being guided
    3. You can use the ForceReply of the Bot API

    Alright. So for 1. I’d say you have some benefits. You can actually guide the user and restrict access to some commands, when he is not in the proper state. So let’s say he wants to buy Popcorn but he’s in the shoestore you’d disallow the command by checking the saved user state.

    For 2. you would always allow the user to use /buy and /buy_popcorn and /buy_shoe. But depending on your answers you just give him a specific amount of possible selections.

    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
    

    This would be allowed, but the user would have to manually write /buy_shoe

    The 3. possible way is using the ForceReply. The user will automatically get an answer to message. So when he uses /buy_shoe he will answer to the last message the bot sent. You will also get the message the user answered to in the message from the api. You can check if the message the user answered to is the proper precondition / proper message for the command and then restrict or allow the command.

    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
    

    It comes down to personal preference, I guess. But all of it has pros and cons and you have to decide if you want to allow specific commands without a precondition.

    This List may not be complete. It could be that there are other ways, I didn’t think about. But these 3 are ways I know of.

    Login or Signup to reply.
  2. I also had this problem where i needed my bot to respond questions based on his last answer to the user, and 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.

    As Loki pointed out, 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 ud83eudd14 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search