logic for parsing messages inside a Telegram bot

I present to your attention my instructions for user interaction with a Telegram bot in various situations.

Telegran bot (long polling) Avandy News, review article, open source GitHub

Main idea: Avandy News Analysis (included in Register of Russian softwaredeveloped by me)

Register of Russian software: How did you get into it?

Bot recipe:

  • Java 17, Spring Boot 3.15 – utensils

  • Postgres – cellar for twisting

  • BotCommand – menu with basic commands, 7 pcs.

  • ReplyKeyboardMarkup – quick buttons, 2 pcs.

  • InlineKeyboardMarkup – to taste

  • Symbols { } – 64 pcs. per liter of coffee

Guests came to us and…

Situation No. 1 (processing Inline buttons)

They want to write any word to the bot to search for headlines containing that word.

Briefly

  1. Creating a thread-safe collection ConcurrentHashMapwhere the key is the unique user id given by Telegram itself (chat_id).

  2. We receive the text of the message that arrived update.getMessage().getText()

  3. We create cases for processing messages based on the logic of your application (for me, such a message, which does not match any case, ends up in the default method. It already has its own checks based on the experience of messages from a couple of thousand users)

  4. Creating a keyboard InlineKeyboardMarkup with buttons

  5. When creating a keyboard, we put the word for a specific guest in the collection clause 1

  6. In section update.hasCallbackQuery() get the name of the button pressed and go to its case

  7. We take a word from the collection and pass it to the desired method for processing. All.

NOT briefly

Previously there were already 5 buttons (simplifying, simplifying..)

Previously there were already 5 buttons (simplifying, simplifying..)

What it looks like in code:

/* 1 шаг. Для того, чтобы слова пользователей, которые пишут боту одновременно,
   не пересекались - делаем потокобезопасную коллекцию 
   где ключ Long это уникальный chat_id, который любезно предоставляет Telegram */
private final Map<Long, String> oneWordFromChat = new ConcurrentHashMap<>();

@Override
public void onUpdateReceived(Update update) {
    // 2 шаг. Гость отправил сообщение и мы его перехватили здесь
    if (update.hasMessage() && update.getMessage().hasText()) {
        String messageText = update.getMessage().getText(); // = "Москва"
        long chatId = update.getMessage().getChatId(); // уникальный id гостя
      
        // Принудительный выход для психов (поверьте - часто прилетает)
        List<String> flugegeheimen = List.of("стоп", "/стоп", "stop", "/stop");
        // toLowerCase :)
        if (flugegeheimen.contains(messageText.toLowerCase())) {
          userStates.remove(chatId); // Сбрасываем состояние бота (об этом позже)
          nextKeyboard(chatId, "Действие отменено"); // Отправляем сообщение гостю
              
        } else if (messageText.startsWith("")) {
        } else if (messageText.equals("")) {
        } else {
            /* Основные команды */
            switch (messageText) {
              case "/settings" -> getSettings(chatId);
              case "/info" -> infoKeyboard(chatId);
              // 3 шаг. Мы попали сюда, т.к. сообщение "Москва" 
              // ни с одним кейсом не совпало
              default -> undefinedKeyboard(chatId, messageText);
            }
        }
    }
}
  1. Asking a question through class SendMessage, InlineKeyboardMarkup – 2 pieces, a thread-safe collection and a beautiful smiley to smooth the situation after such a direct question.

// 4 шаг. Показываем клавиатуру InlineKeyboardMarkup с двумя кнопками
private void undefinedKeyboard(long chatId, String word) {
  // 5 шаг. Кладём слово для конкретного гостя в ранее созданную коллекцию
  oneWordFromChat.put(chatId, word); // word = "Москва"
  
  // Делаем двухслойный торт из кнопок
  Map<String, String> floor1 = new HashMap<>();
  Map<String, String> floor2 = new HashMap<>();

  // Понятно озвучиваем ингридиенты для себя и гостей
  floor1.put("ADD_KEYWORD_FROM_CHAT", "Сохранить для автопоиска");
  floor2.put("SEARCH_BY_WORD", "Искать новости");

  // код maker на GitHub
  sendMessage(chatId, "Что с этим сделать? " + "\uD83E\uDDD0", 
              InlineKeyboards.maker(floor1, floor2));
}
  1. Guests click on the “Search news” button, but not a fact, and

if (update.hasMessage() && update.getMessage().hasText()) {
    // здесь сейчас пусто, поэтому идём дальше
} else if (update.hasCallbackQuery()) {
  // 6 шаг. А вот здесь уже ловим нажатую кнопку
  String callbackData = update.getCallbackQuery().getData(); // = "SEARCH_BY_WORD"

  switch (callbackData) {
      case "JOHN_COFFEY_MAM" -> drinkCoffee(chatId, "See The Green mile");
      // зашли сюда
      case "SEARCH_BY_WORD" ->
        // 7 шаг. Передаём слово из коллекции в метод поиска
        wordSearch(chatId, oneWordFromChat.get(chatId));
  }

}
  1. Then they observe the following response (the type of news is possible in 5 options). As you can see, Moscow’s endings are different, not just as requested, but that’s a completely different story.

    Noooooo, without you..

    Noooooo, without you..

Situation #2 (waiting for user input)

Let's say a guest decides to save a word for autosearch.

He presses Add and the bot goes into a state of waiting for keyboard input for a specific user (here, too, there are a lot of checks, the user doesn’t do anything).

Briefly

  1. Pressing a button

  2. Transferring the bot to the desired state by adding it to a thread-safe collection for a specific guest

  3. Waiting for message to be sent

  4. Processing a message for a specific guest and for a specific state

NOT quite briefly

To section hasCallbackQuery flies callbackData equal ADD_KEYWORD (that’s what we called this button inside the bot) and our bot is waiting for the message to be sent, i.e. its status for a specific user is included on ADD_KEYWORDS

// Состояния бота
public enum UserState {
    SEND_FEEDBACK,
    //..
    ADD_KEYWORDS
}
/* Класс бота */
// 1. Коллекция состояний для каждого пользователя где Long = chatId
private final Map<Long, UserState> userStates = new ConcurrentHashMap<>();

  if (update.hasMessage() && update.getMessage().hasText()) {
      // здесь сейчас пусто
  } else if (update.hasCallbackQuery()) {
    
    // 2. Нажав на кнопку мы попадаем в этот кейс, т.к. "Добавить" = "ADD_KEYWORD"
    case "ADD_KEYWORD" -> setBotState(chatId, UserState.ADD_KEYWORDS, addInListText);
    
    // Сброс состояния бота для конкретного пользователя если он нажмёт "Отмена"
    case "CANCEL" -> userStates.remove(chatId);

  }

  // 3. Установка состояния бота
  private void setBotState(Long chatId, UserState state, String text) {
      userStates.put(chatId, state);
      // 4. Здесь бот и ожидает ввода текста. Тут же можем отменить ввод.
      cancelKeyboard(chatId, text);
  }

  // Кнопка отмены ввода слов
  private void cancelKeyboard(long chatId, String text) {
      Map<String, String> buttons = new LinkedHashMap<>();
      buttons.put("CANCEL", cancelButtonText);
      sendMessage(chatId, text, InlineKeyboards.maker(buttons));
  }
  
  // Клавиатура раздела с ключевыми словами для поиска
  private void keywordsListKeyboard(long chatId, String text) {
      Map<String, String> buttons1 = new LinkedHashMap<>();
      Map<String, String> buttons2 = new LinkedHashMap<>();
  
      buttons1.put("DELETE_KEYWORD", delText);
      buttons1.put("ADD_KEYWORD", addText);
      buttons2.put("SET_PERIOD", intervalText);
      buttons2.put("FIND_BY_KEYWORDS", searchText);
      
    sendMessage(chatId, text, InlineKeyboards.maker(buttons1, buttons2));
  }  
}

After entering text, when the bot is able ADD_KEYWORDS, we catch the entered message already in the section update.hasMessage() where it is processed

// Обработка сообщения
if (update.hasMessage() && update.getMessage().hasText()) {
      String messageText = update.getMessage().getText();
      long chatId = update.getMessage().getChatId();
      
      // 5. Берём состояние бота для конкретного пользователя
      UserState userState = userStates.get(chatId);
      
      // Состояние бота сейчас - это добавление слов для автопоиска
      if (UserState.ADD_KEYWORDS.equals(userState)) {
        // Обрабатываем
        String keywords = messageText.trim().toLowerCase();
        addKeywords(chatId, keywords);
      }

    // Сброс состояния
    userStates.remove(chatId);
  
} else if (update.hasCallbackQuery()) {
  //..
}

Situation No. 3 (processing a message with a photo)

The guest wanted to write us his impressions of the evening and attach a photo.

Briefly

  1. Pressing a button

  2. Transferring the bot to the desired state by adding it to a thread-safe collection for a specific guest

  3. Waiting for message to be sent

  4. Separate processing of messages with photos

No

// После нажатия кнопки "Написать отзыв"
if (update.hasMessage() && update.getMessage().hasText()) {}
else if (update.hasCallbackQuery()) {
      // Устанавливаем статус бота в режим SEND_FEEDBACK для конкретного гостя
      case "FEEDBACK" -> setBotState(chatId, UserState.SEND_FEEDBACK);
}
// Обработка сообщения с фото
@Override
public void onUpdateReceived(Update update) {
    if (update.hasMessage() && update.getMessage().hasPhoto()) {
          long chatId = update.getMessage().getChatId();
          // Получаем состояние бота
          UserState userState = userStates.get(chatId);

        // update содержит скриншот
        if (UserState.SEND_FEEDBACK.equals(userState)) {
            // Отправка сообщения разработчику с приложением скриншота
            sendFeedbackWithPhoto(update);
            // Сброс состояния бота
            userStates.remove(chatId);
            // Уведомляем, что отправка успешна
            sendMessage(chatId, "Доставлено ✔️");
        }            
    } 
    else if (update.hasMessage() && update.getMessage().hasText()) {} 
    else if (update.hasCallbackQuery()) {}
}

// Отправка отзыва повару с приложением скриншота (если можно проще, то скажите как)
private void sendFeedbackWithPhoto(Update update) {
    long chatIdFrom = update.getMessage().getChatId();
    String userName = userRepository.findNameByChatId(chatIdFrom);
    String caption = update.getMessage().getCaption();
    if (caption == null) caption = "не удосужился..";
    List<PhotoSize> photos = update.getMessage().getPhoto();

    SendPhoto sendPhoto = new SendPhoto();
    sendPhoto.setChatId(OWNER_ID);
    sendPhoto.setCaption("Message from " + userName + ", " + chatIdFrom + ":\n" + caption);

    List<PhotoSize> photo = update.getMessage().getPhoto();
    if (photo == null) return;
    try {
        String fileId = Objects.requireNonNull(photos.stream().max(Comparator.comparing(PhotoSize::getFileSize))
                .orElse(null)).getFileId();

        URL url = new URL("https://api.telegram.org/bot" + TOKEN + "/getFile?file_id=" + fileId);

        BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
        String res = in.readLine();
        String filePath = new JSONObject(res).getJSONObject("result").getString("file_path");
        String urlPhoto = "https://api.telegram.org/file/bot" + TOKEN + "/" + filePath;

        URL url2 = new URL(urlPhoto);
        BufferedImage img = ImageIO.read(url2);

        ByteArrayOutputStream os = new ByteArrayOutputStream();
        ImageIO.write(img, "jpg", os);
        InputStream is = new ByteArrayInputStream(os.toByteArray());
        sendPhoto.setPhoto(new InputFile(is, caption));

        execute(sendPhoto);

    } catch (IOException | TelegramApiException e) {
        log.error(e.getMessage());
    }
}

Result

While the wife doesn't see

While the wife doesn't see

Situation No. 4 (sending a message to a specific user from the bot owner)

If the developer personally wants to send a chat message to a specific guest, then you can do this.

We write Emily a message in the chat like this:

@123457 What should have been done? Reinstall Windows?

  1. @ – you can use any symbol or word, I chose this one

  2. without a space after @ we indicate the unique chat_it of the guest (we know it or see it when a review arrives)

  3. put a space and write a message that the guest with the specified chat_it will see

// Перехватываем простое сообщение без фото, без нажатия Inline кнопок
if (update.hasMessage() && update.getMessage().hasText()) {
  long chatId = update.getMessage().getChatId();

  // Проверямем, что сообщение отправляет именно владелец бота
  if (messageText.startsWith("@") && config.getBotOwner() == chatId) {
    // берём chat_id от собаки до пробела ("что значит открой собаку?")
    long chatToSend = Long.parseLong(messageText
                                     .substring(1, messageText.indexOf(" ")));
    // а здесь берём всё после пробела
    String textToSend = messageText.substring(messageText.indexOf(" "));
    sendMessage(chatToSend, textToSend);
  }
  
}
For example, I’m writing to myself, so everything is in one chat

For example, I’m writing to myself, so everything is in one chat

Situation #5 (Menu and Reply Keyboard)

Menu commands are processed in the message processing section without photos and without pressed buttons.

Click on the command /settingsgo to the corresponding case

if (update.hasMessage() && update.getMessage().hasText()) {
   switch (messageText) {
     case "/settings" -> getSettings(chatId);
     case "/keywords", "/list_key" -> showKeywordsList(chatId);
     case "/info" -> infoKeyboard(chatId);
   }
}

To quickly call frequently used or simply basic functionality, use ReplyKeyboardMarkup

// Инициализации клавиатуры
getReplyKeyboard(chatId, textToSend);

// Создание клавиатуры
private void getReplyKeyboard(long chatId, String textToSend) {
  KeyboardRow row = new KeyboardRow();
  row.add("Все новости");
  row.add("Top 20");
  row.add("По словам");
  
  sendMessage(chatId, textToSend, ReplyKeyboards.replyKeyboardMaker(row));
}
// Класс ReplyKeyboards
import org.springframework.stereotype.Component;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.ReplyKeyboardMarkup;
import org.telegram.telegrambots.meta.api.objects.replykeyboard.buttons.KeyboardRow;

import java.util.ArrayList;
import java.util.List;

@Component
public class ReplyKeyboards {
    
  public static ReplyKeyboardMarkup replyKeyboardMaker(KeyboardRow row) {
        ReplyKeyboardMarkup keyboardMarkup = new ReplyKeyboardMarkup();
        keyboardMarkup.setResizeKeyboard(true);

        List<KeyboardRow> keyboardRows = new ArrayList<>();
        keyboardRows.add(row);
        keyboardMarkup.setKeyboard(keyboardRows);
        return keyboardMarkup;
    }
}

After clicking on one of these buttons there will be processing a simple message with text and not like after clicking on the Inline Keyboards buttons

if (update.hasMessage() && update.getMessage().hasText()) {  
  if (messageText.equals("По словам")) {
    findNewsByKeywordsManual(chatId);
  } else if (messageText.equals("Top 20")) {
    showTop(chatId);
  } else if (messageText.equals("Все новости")) {
    fullSearch(chatId);
  } else {
    switch (messageText) {
     case "/settings" -> getSettings(chatId);
     case "/keywords", "/list_key" -> showKeywordsList(chatId);
     case "/info" -> infoKeyboard(chatId);
    }
  }
}

Let's summarize

This is my first bot, experimental, so to speak. I've been doing it for a long time, learning. When I make a new bot, of course, it will be of better quality, although I did my best here too.

If you have any suggestions for improving the code or any other useful situations, write in the comments, I’ll try to apply it. Or what could be a better use of technology webHook instead of longPolling. Link to source attached.

You give open source and thanks to Pavel Durov for BotFather!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *