@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 along the chain. Update is the most important thing when working with a bot and should not be lost, because with the help of the information stored in the update, we will find out to which user and to which chat the answer should be sent. To generate responses to the user, I wrote a separate class. It can send normal text message, inline keyboard message, picture message and reply keyboard message. The inline keyboard looks like this: It defines buttons, by clicking on which, the user sends a callback to the server, which can be processed in much 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 "Price" button, we set the description "/price" for the callback, which we can later get from the update. Further 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 fact, it replaces the user's typing. By clicking on the "Library" button, you 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 the requirements. This is terribly convenient, since keyboards can 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 were invented through the slash symbol: /library, /help, /game, etc. Otherwise, we would have to process any garbage that the user could write. Actually, MessageHandler was written for this:
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 consider the work of the parser and the library. If you send a link to a game in the Google Play store to the bot, then a special handler will automatically work . In response, the user will receive information about the game in the following form: Along with this, a method will be called that will try to add the game to the bot's library (first to the local map, then to the -> json file). If the game is already in the library, then a check will be made (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 records will be made. If there was no game in the library at all, then it is first written to the local map (object like tyk ), and then written to the json file, since if the application unexpectedly closes on the server, the data will be lost, and using the file they can always be read. 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 the data from the file into a temporary map, which is then "copied" into a full-fledged one in order to maintain case insensitivity when searching for a game in the file (by writing tITan QuEST, the bot will still find the Titan Quest game in the library). It was not possible to find another solution, these are the features of deserialization using Jackson. So, with each request by reference, the game is added to the library if possible, and the library is thereby expanded. Further, information about a particular game can be obtained using the /library Game_Name command.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. In the course of work, he also applied the skills acquired here in the course of solving problems. For example, a list of names of random games in the library (the option is available with 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 handle links? It checks 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. It will not work to deceive 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 I had to solve the 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 carries out a "redirect" to the Russian version of the page (the project was still aimed at our audience). To do this, at the end of the link, you need to carefully add the hl: &hl=ru parameter 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 get an HTML document ready for parsing and parsing, but this is already beyond the scope of this article. parser code here. The parser itself gets the necessary information and creates an object with the game, which is later added to the library if necessary. <h2>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 /game + link, it checks if the link belongs to Google Play. If the link is valid, it connects via Jsoup and gets an HTML document. This document is parsed 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 the local storage (if the game is not there yet) and immediately written to a file to avoid data loss. The 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 by 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 match the name of the game in the Google Play store, except for 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 plan to write their own bot and host it for free on heroku in the future, I will give a couple of recommendations for solving the difficulties that you may encounter (because I encountered them myself). Unfortunately, due to the nature of heroku, the bot's library is constantly "reset" every 24 hours. My plan doesn't support file storage on heroku servers, so it just pulls my games file from github. There were several solutions: use the database, or look for another server that would store this game file. I decided not to do anything for now, because in fact the bot is not so useful. I needed it rather to get a full experience, which I basically achieved. So here are the recommendations for heroku: I needed it rather to get a full experience, which I basically achieved. So here are the recommendations for heroku: I needed it rather to get a full experience, which I basically achieved. So here are the recommendations for heroku:
-
You will most likely need to sign up for 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 following the pattern , where in the mainClass tag specify 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 build everything for you.
-
It is advisable to add 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 toggle the slider, and then make sure it's ON (due to the fact that I didn't do this, my bot didn't work and I racked my brains for a couple of days and made a lot of unnecessary gestures).
-
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 token there):
- received a fully working project written in Java;
- learned to work with a third-party API (Telegram Bot API);
- in practice, delved 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 command of the development environment (IDEA);
- learned to work with git and learned the value of gitignore;
- gained skills in parsing web pages (Jsoup library);
- studied and used several design patterns;
- developed a sense and desire to improve the code (refactoring);
- learned to find solutions on the net and not be shy to ask questions that could not be answered.
GO TO FULL VERSION