@Override
public void onUpdateReceived(Update update) {
UpdatesReceiver.handleUpdates(update);
}
which I pass to the handler (own UpdatesReceiver class):
public static void handleUpdates(Update update) {
...
if (update.hasMessage() && update.getMessage().hasText()){
log.info("[Update (id {}) типа \"Текстовое сообщение\"]", update.getUpdateId());
new TextMessageHandler(update, replyGenerator).handleTextMessage();
}
else if (update.hasCallbackQuery()) {
//логгирование
new CallbackQueryHandler(update, replyGenerator).handleCallBackQuery();
}
else {
//логгирование
replyGenerator.sendTextMessage(update.getMessage().getChatId(), "Я могу принимать только текстовые messages!");
}
}
UpdatesReceiver is a central handler that, depending on the type of update, transfers control to another specialized handler: TextMessageHandler or CallbackQueryHandler, to whose constructors I pass update further down the chain. Update is the most important thing when working with a bot and cannot be lost, because with the help of the information stored in the update, we find out which user and to which chat the response should be sent. To generate responses to the user, I wrote a separate class. It can send regular text message, message with inline keyboard, message with picture and message with reply keyboard. An inline keyboard looks like this: It defines buttons that, by clicking on them, the user sends a callback to the server, which can be processed in almost the same way as regular messages. To “maintain” it you need your own handler. We set an action for each button, which is then written to the Update object. Those. for the "Cost" button we set the description "/price" for the callback, which we can later get from the update. Next, in a separate class, I can already process this callback:
public void handleCallBackQuery() {
String call_data = update.getCallbackQuery().getData();
long message_id = update.getCallbackQuery().getMessage().getMessageId();
long chat_id = update.getCallbackQuery().getMessage().getChatId();
switch (call_date){
case "/price" :
//тут что-то сделать
break;
...
The Reply keyboard looks like this: And in essence, it replaces the user's typing. Clicking the "Library" button will quickly send a "Library" message to the bot. For each type of keyboard, I wrote my own class, implementing the Builder pattern: inline and reply . As a result, you can essentially “draw” the desired keyboard depending on your requirements. This is terribly convenient, since keyboards may be different, but the principle remains the same. Here is an intuitive method to send a message with an inline keyboard:
public synchronized void sendInlineKeyboardMessage(long chat_id, String gameTitle) {
SendMessage keyboard = InlineKeyboardMarkupBuilder.create(chat_id)
.setText("Вы может узнать следующую информацию об игре " + gameTitle)
.row()
.button("Стоимость " + "\uD83D\uDCB0", "/price " + gameTitle)
.button("Обновлено " + "\uD83D\uDDD3", "/updated " + gameTitle)
.button("Версия " + "\uD83D\uDEE0", "/version " + gameTitle)
.endRow()
.row()
.button("Требования " + "\uD83D\uDCF5", "/requirements " + gameTitle)
.button("Покупки " + "\uD83D\uDED2", "/iap " + gameTitle)
.button("Размер " + "\uD83D\uDD0E", "/size " + gameTitle)
.endRow()
.row()
.button("Получить всю информацию об игре" + "\uD83D\uDD79", "/all " + gameTitle)
.endRow()
.row()
.button("Скрыть клавиатуру", "close")
.endRow()
.build();
try {
execute(keyboard);
} catch (TelegramApiException e) {
log.error("[Не удалось отправить сообщение с -inline- клавиатурой]: {}", e.getMessage());
}
}
To give the bot strict functionality, special commands using the slash character were invented: /library, /help, /game, etc. Otherwise, we would have to process any garbage that the user might write. Actually, this is what MessageHandler was written for:
if (message.equals(ChatCommands.START.getDescription())) {
replyGenerator.sendTextMessage(chat_id, new StartMessageHandler().reply());
replyGenerator.sendReplyKeyboardMessage(chat_id);
}
else if (message.equals(ChatCommands.HELP.getDescription())
|| message.equalsIgnoreCase("Помощь")) {
replyGenerator.sendTextMessage(chat_id, new HelpMessageHandler().reply());
}
...
Thus, depending on what command you send to the bot, a special handler will be included in the work. Let's go further and look at the work of the parser and library. If you send the bot a link to a game in the Google Play store, a special handler will automatically work . In response, the user will receive information about the game in the following form: At the same time, a method will be called that will try to add the game to the bot’s library (first to the local map, then to -> json file). If the game is already in the library, then a check will be carried out (as in a regular hashmap), and if the field data (for example, the version number has changed), then the game in the library will be overwritten. If no changes are detected, then no entries will be made. If there was no game in the library at all, then it is first written to the local map (an object like tyk ), and then written to a json file, since if the application on the server is unexpectedly closed, the data will be lost, but it can always be read using the file. Actually, when the program starts, the library is always loaded for the first time from a file from a static block:
static {
TypeFactory typeFactory = mapper.getTypeFactory();
MapType mapType = typeFactory.constructMapType(ConcurrentSkipListMap.class, String.class, GooglePlayGame.class);
try {
Path path = Paths.get(LIBRARY_PATH);
if (!Files.exists(path)) {
Files.createDirectories(path.getParent());
Files.createFile(path);
log.info("[Файл библиотеки создан]");
}
else {
ConcurrentMap<string, googleplaygame=""> temporary = mapper.readValue(new File(LIBRARY_PATH), mapType);
games.putAll(temporary);
log.info("[Количество игр в загруженной библиотеке] = " + games.size());
}
}
catch (IOException e) {
log.error("[Ошибка при чтении/записи file] {}", e.getMessage());
}
}
Here you additionally have to read data from the file into a temporary map, which is then “copied” into a full map in order to maintain case insensitivity when searching for a game in the file (by writing tITan QuEST, the bot will still find the game Titan Quest in the library). It was not possible to find another solution, these are the features of deserialization using Jackson. So, with each request for a link, the game is added to the library, if possible, and the library thereby expands. Further information about a specific game can be obtained using the command /libraryGame_Name. You can find out both a specific parameter (for example, the current version) and all parameters at once. This is implemented using the inline keyboard, which was discussed earlier. During the work, I also applied the skills acquired here while solving problems. For example, a list of names of random games located in the library (the option is available using the /library command):
private String getRandomTitles(){
if (LibraryService.getLibrary().size() < 10){
return String.join("\n", LibraryService.getLibrary().keySet());
}
List<string> keys = new ArrayList<>(LibraryService.getLibrary().keySet());
Collections.shuffle(keys);
List<string> randomKeys = keys.subList(0, 10);
return String.join("\n", randomKeys);
}
How does the bot process links? It checks them to see if they belong to Google Play (host, protocol, port):
private static class GooglePlayCorrectURL {
private static final String VALID_HOST = "play.google.com";
private static final String VALID_PROTOCOL = "https";
private static final int VALID_PORT = -1;
private static boolean isLinkValid(URI link) {
return (isHostExist(link) && isProtocolExist(link) && link.getPort() == VALID_PORT);
}
private static boolean isProtocolExist(URI link) {
if (link.getScheme() != null) {
return link.getScheme().equals(VALID_PROTOCOL);
}
else {
return false;
}
}
private static boolean isHostExist(URI link) {
if (link.getHost() != null) {
return link.getHost().equals(VALID_HOST);
}
else {
return false;
}
}
If everything is in order, then the bot connects via a link using the Jsoup library, which allows you to get the HTML code of the page, which is subject to further analysis and parsing. You won't be able to fool the bot with an incorrect or harmful link.
if (GooglePlayCorrectURL.isLinkValid(link)){
if (!link.getPath().contains("apps")){
throw new InvalidGooglePlayLinkException("К сожалению, бот работает исключительно с играми. Введите другую ссылку.");
}
URL = forceToRusLocalization(URL);
document = Jsoup.connect(URL).get();
}
else {
throw new NotGooglePlayLinkException();
}
...
Here we had to solve a problem with regional settings. The bot connects to the Google Play store from a server located in Europe, so the page in the Google Play store opens in the appropriate language. I had to write a crutch that forcibly “redirects” to the Russian version of the page (the project was, after all, aimed at our audience). To do this, at the end of the link you need to carefully add the parameter hl: &hl=ru in the GET request to the Google Play server .
private String forceToRusLocalization(String URL) {
if (URL.endsWith("&hl=ru")){
return URL;
}
else {
if (URL.contains("&hl=")){
URL = URL.replace(
URL.substring(URL.length()-"&hl=ru".length()), "&hl=ru");
}
else {
URL += "&hl=ru";
}
}
return URL;
}
After a successful connection, we receive an HTML document ready for analysis and parsing, but this is beyond the scope of this article. The parser code is here . The parser itself retrieves the necessary information and creates an object with the game, which is later added to the library if necessary. <h2>To summarize</h2>The bot supports several commands that contain certain functionality. It receives messages from the user and matches them with its commands. If it's a link or the /game + link command, it checks that link to see if it belongs to Google Play. If the link is correct, it connects via Jsoup and receives the HTML document. This document is analyzed based on the written parser. The necessary information about the game is extracted from the document, and then the object with the game is filled with this data. Next, the object with the game is placed in local storage (if the game is not there yet) and immediately written to a file to avoid data loss. A game recorded in the library (the name of the game is the key for the map, the object with the game is the value for the map) can be obtained using the command /library Game_name. If the specified game is found in the bot's library, the user will be returned an inline keyboard, with which he can get information about the game. If the game is not found, you must either make sure the name is spelled correctly (it must completely match the name of the game in the Google Play store, except for the case), or add the game to the library by sending the bot a link to the game. I deployed the bot on heroku and for those who in the future plan to write their own bot and host it for free on heroku, I will give a couple of recommendations for solving the difficulties that you may encounter (since I encountered them myself). Unfortunately, due to the nature of Heroku, the bot library is constantly “reset” once every 24 hours. My plan does not support storing files on Heroku servers, so it simply pulls my game file from Github. There were several solutions: use a database, or look for another server that would store this file with the game. I decided not to do anything for now, since essentially the bot is not that useful. I needed it rather to gain a full experience, which is basically what I achieved. So, recommendations for Heroku:
-
You will most likely have to register on heroku using a VPN if you live in Russia.
-
At the root of the project you need to put a file without an extension called Procfile. Its content should be like this: https://github.com/miroha/Telegram-Bot/blob/master/Procfile
-
In pom.xml, add the following lines according to the example , where in the mainClass tag indicate the path to the class that contains the main method: bot.BotApplication (if the BotApplication class is in the bot folder).
-
Do not build any project using mvn package commands, etc., Heroku will assemble everything for you.
-
It is advisable to add a gitignore to the project, for example this:
# Log file *.log # Compiled resources target # Tests test # IDEA files .idea *.iml
-
Actually upload the project to github, and then connect the repository to Heroku (or use other methods, there are 3 of them, if I’m not mistaken).
-
If the download was successful ("Build succeeded"), be sure to go to Configure Dynos:
and switch the slider, and then make sure that it is in the ON position (due to the fact that I did not do this, my bot did not work and I racked my brain for a couple of days and made a lot of unnecessary movements).
-
Hide the bot token on Github. To do this, you need to get the token from the environment variable:
public class Bot extends TelegramLongPollingBot { private static final String BOT_TOKEN = System.getenv("TOKEN"); @Override public String getBotToken() { return BOT_TOKEN; } ... }
And then after deploying the bot, set this variable in the Heroku dashboard in the Settings tab (to the right of TOKEN there will be a VALUE field, copy your bot’s token there):
- received a fully working project written in Java;
- learned to work with third-party API (Telegram Bot API);
- in practice, I delved deeper into serialization, worked a lot with JSON and the Jackson library (initially I used GSON, but there were problems with it);
- strengthened my skills when working with files, got acquainted with Java NIO;
- learned to work with configuration .xml files and accustomed myself to logging;
- improved proficiency in development environment (IDEA);
- learned to work with git and learned the value of gitignore;
- gained skills in web page parsing (Jsoup library);
- learned and used several design patterns;
- developed a sense and desire to improve code (refactoring);
- I learned to find solutions online and not to be shy about asking questions to which I couldn’t find an answer.
GO TO FULL VERSION