@Override
public void onUpdateReceived(Update update) {
UpdatesReceiver.handleUpdates(update);
}
które przekazuję do modułu obsługi (własna klasa UpdatesReceiver):
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(), "Я могу принимать только текстовые wiadomości!");
}
}
UpdatesReceiver to centralny moduł obsługi, który w zależności od rodzaju aktualizacji przekazuje kontrolę do innego wyspecjalizowanego modułu obsługi: TextMessageHandler lub CallbackQueryHandler, którym konstruktorom przekazuję aktualizację w dalszej części łańcucha. Aktualizacja jest najważniejszą rzeczą podczas pracy z botem i nie powinna zostać zagubiona, ponieważ przy pomocy informacji zapisanych w aktualizacji dowiadujemy się, do jakiego użytkownika i na jaki czat należy wysłać odpowiedź. Aby wygenerować odpowiedzi dla użytkownika, napisałem osobną klasę. Może wysyłać zwykłe wiadomości tekstowe, wiadomości z wbudowaną klawiaturą, wiadomości ze zdjęciem i wiadomości z klawiaturą zwrotną. Klawiatura wbudowana wygląda następująco: Definiuje przyciski, których kliknięcie powoduje wysłanie przez użytkownika wywołania zwrotnego do serwera, które można przetworzyć niemal w taki sam sposób, jak zwykłe wiadomości. Aby go „utrzymać”, potrzebujesz własnego handlera. Dla każdego przycisku ustawiamy akcję, która następnie jest zapisywana w obiekcie Update. Te. dla przycisku „Koszt” ustawiamy opis „/cena” dla wywołania zwrotnego, który możemy później uzyskać z aktualizacji. Następnie w osobnej klasie mogę już przetworzyć to wywołanie zwrotne:
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;
...
Klawiatura odpowiedzi wygląda następująco: Zasadniczo zastępuje ona pisanie przez użytkownika. Kliknięcie przycisku „Biblioteka” spowoduje szybkie wysłanie do bota wiadomości „Biblioteka”. Dla każdego typu klawiatury napisałem własną klasę, implementując wzorzec Buildera: inline i respond . W rezultacie możesz zasadniczo „narysować” żądaną klawiaturę w zależności od wymagań. Jest to strasznie wygodne, ponieważ klawiatury mogą być różne, ale zasada pozostaje ta sama. Oto intuicyjna metoda wysyłania wiadomości za pomocą wbudowanej klawiatury:
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());
}
}
Aby zapewnić botowi ścisłą funkcjonalność, wymyślono specjalne polecenia wykorzystujące znak ukośnika: /library, /help, /game itp. W przeciwnym razie musielibyśmy przetworzyć wszelkie śmieci, które użytkownik mógłby zapisać. Właściwie do tego właśnie napisano MessageHandler:
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());
}
...
Zatem w zależności od tego, jakie polecenie wyślesz botowi, do pracy zostanie włączony specjalny moduł obsługi. Pójdźmy dalej i przyjrzyjmy się pracy parsera i biblioteki. Jeśli wyślesz botowi link do gry w sklepie Google Play, automatycznie zadziała specjalny moduł obsługi . W odpowiedzi użytkownik otrzyma informację o grze w następującej formie: Jednocześnie zostanie wywołana metoda, która spróbuje dodać grę do biblioteki bota (najpierw do mapy lokalnej, następnie do -> pliku json ). Jeśli gra znajduje się już w bibliotece, to zostanie przeprowadzone sprawdzenie (jak w zwykłej hashmapie), a jeśli zmieniły się dane w polu (np. numer wersji), to gra w bibliotece zostanie nadpisana. Jeśli nie zostaną wykryte żadne zmiany, żadne wpisy nie zostaną dokonane. Jeśli w bibliotece w ogóle nie było gry, to najpierw jest ona zapisywana na lokalną mapę (obiekt typu tyk ), a następnie zapisywana do pliku json, gdyż w przypadku nieoczekiwanego zamknięcia aplikacji na serwerze dane zostaną utracone, ale zawsze można je odczytać za pomocą pliku. Właściwie przy uruchomieniu programu biblioteka jest zawsze ładowana po raz pierwszy z pliku z bloku statycznego:
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("[Ошибка при чтении/записи plik] {}", e.getMessage());
}
}
Tutaj dodatkowo trzeba wczytać dane z pliku do mapy tymczasowej, która następnie jest „kopiowana” do pełnej mapy, aby zachować rozróżnianie wielkości liter przy wyszukiwaniu gry w pliku (wpisując tITan QuEST, bot i tak znajdzie gra Titan Quest w bibliotece). Nie udało się znaleźć innego rozwiązania, takie są cechy deserializacji przy użyciu Jacksona. Tak więc przy każdej prośbie o link gra jest dodawana do biblioteki, jeśli to możliwe, i w ten sposób biblioteka się powiększa. Dalsze informacje na temat konkretnej gry można uzyskać za pomocą polecenia /libraryGame_Name. Możesz znaleźć zarówno konkretny parametr (na przykład aktualną wersję), jak i wszystkie parametry na raz. Jest to realizowane za pomocą wbudowanej klawiatury, co zostało omówione wcześniej. W trakcie pracy wykorzystywałem także zdobyte tu umiejętności przy rozwiązywaniu problemów. Przykładowo lista nazw losowych gier znajdujących się w bibliotece (opcja dostępna przy pomocy komendy /library):
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);
}
Jak bot przetwarza linki? Sprawdza je, aby sprawdzić, czy należą do Google Play (host, protokół, 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;
}
}
Jeśli wszystko jest w porządku, bot łączy się poprzez link korzystając z biblioteki Jsoup, która pozwala na pobranie kodu HTML strony, który podlega dalszej analizie i parsowaniu. Nie oszukasz bota nieprawidłowym lub szkodliwym linkiem.
if (GooglePlayCorrectURL.isLinkValid(link)){
if (!link.getPath().contains("apps")){
throw new InvalidGooglePlayLinkException("К сожалению, бот работает исключительно с играми. Введите другую ссылку.");
}
URL = forceToRusLocalization(URL);
document = Jsoup.connect(URL).get();
}
else {
throw new NotGooglePlayLinkException();
}
...
Tutaj musieliśmy rozwiązać problem z ustawieniami regionalnymi. Bot łączy się ze sklepem Google Play z serwera zlokalizowanego w Europie, dzięki czemu strona w sklepie Google Play otwiera się w odpowiednim języku. Musiałem napisać kulę, która wymusiłaby „przekierowanie” na rosyjską wersję strony (projekt był przecież skierowany do naszych odbiorców). Aby to zrobić należy na końcu linku ostrożnie dodać parametr hl: &hl=ru w żądaniu GET do serwera Google Play .
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;
}
Po udanym połączeniu otrzymujemy dokument HTML gotowy do analizy i parsowania, ale to wykracza poza zakres tego artykułu. Kod parsera znajduje się tutaj . Parser sam pobiera niezbędne informacje i tworzy z grą obiekt, który w razie potrzeby jest później dodawany do biblioteki. <h2>Podsumowując</h2>Bot obsługuje kilka poleceń, które zawierają określoną funkcjonalność. Otrzymuje wiadomości od użytkownika i dopasowuje je do swoich poleceń. Jeśli jest to link lub polecenie /game + link, sprawdza ten link, aby sprawdzić, czy należy do Google Play. Jeśli link jest poprawny, łączy się poprzez Jsoup i odbiera dokument HTML. Dokument ten jest analizowany w oparciu o zapisany parser. Z dokumentu pobierane są niezbędne informacje o grze, a następnie obiekt z grą jest tymi danymi wypełniany. Następnie obiekt z grą umieszczany jest w pamięci lokalnej (jeśli gry jeszcze tam nie ma) i od razu zapisywany do pliku, aby uniknąć utraty danych. Grę zapisaną w bibliotece (nazwa gry jest kluczem do mapy, obiekt z grą jest wartością dla mapy) można uzyskać za pomocą polecenia /library nazwa_gry. Jeśli określona gra zostanie znaleziona w bibliotece bota, użytkownikowi zostanie zwrócona wbudowana klawiatura, za pomocą której będzie mógł uzyskać informacje o grze. Jeśli gra nie zostanie znaleziona, musisz albo upewnić się, że nazwa jest wpisana poprawnie (musi całkowicie odpowiadać nazwie gry w sklepie Google Play, za wyjątkiem przypadku) lub dodać grę do biblioteki wysyłając bota link do gry. Wdrożyłem bota na heroku i dla tych, którzy w przyszłości planują napisać własnego bota i hostować go za darmo na heroku, dam kilka zaleceń dotyczących rozwiązania trudności, które możesz napotkać (ponieważ sam je spotkałem). Niestety, ze względu na naturę Heroku, biblioteka botów jest stale „resetowana” co 24 godziny. Mój plan nie obsługuje przechowywania plików na serwerach Heroku, więc po prostu pobiera plik mojej gry z Githuba. Rozwiązań było kilka: użyj bazy danych lub poszukaj innego serwera, który będzie przechowywał ten plik z grą. Postanowiłem na razie nic nie robić, ponieważ w zasadzie bot nie jest zbyt przydatny. Potrzebowałem tego raczej do zdobycia pełnego doświadczenia i to w zasadzie udało mi się osiągnąć. Zatem rekomendacje dla Heroku:
-
Jeśli mieszkasz w Rosji, najprawdopodobniej będziesz musiał zarejestrować się na heroku za pomocą VPN.
-
W katalogu głównym projektu musisz umieścić plik bez rozszerzenia o nazwie Procfile. Jego zawartość powinna wyglądać następująco: https://github.com/miroha/Telegram-Bot/blob/master/Procfile
-
W pliku pom.xml dodaj linie zgodnie z przykładem , gdzie w tagu mainClass wskazujesz ścieżkę do klasy zawierającej metodę główną: bot.BotApplication (jeśli klasa BotApplication znajduje się w folderze bot).
-
Nie buduj żadnego projektu za pomocą poleceń pakietu mvn itp., Heroku zmontuje wszystko za Ciebie.
-
Wskazane jest dodanie do projektu gitignore, na przykład to:
# Log file *.log # Compiled resources target # Tests test # IDEA files .idea *.iml
-
Właściwie prześlij projekt na github, a następnie podłącz repozytorium do Heroku (lub użyj innych metod, są ich 3, jeśli się nie mylę).
-
Jeśli pobieranie przebiegło pomyślnie („Kompilacja powiodła się”), koniecznie przejdź do opcji Konfiguruj Dynos:
i przełącz suwak, a następnie upewnij się, że jest w pozycji ON (ponieważ tego nie zrobiłem, mój bot nie działał i kręciłem się po głowie przez kilka dni i wykonałem wiele niepotrzebnych ruchów ).
-
Ukryj token bota na Githubie. Aby to zrobić, musisz pobrać token ze zmiennej środowiskowej:
public class Bot extends TelegramLongPollingBot { private static final String BOT_TOKEN = System.getenv("TOKEN"); @Override public String getBotToken() { return BOT_TOKEN; } ... }
A następnie po wdrożeniu bota ustaw tę zmienną w dashboardzie Heroku w zakładce Ustawienia (na prawo od TOKEN pojawi się pole WARTOŚĆ, skopiuj tam token swojego bota):
- otrzymał w pełni działający projekt napisany w Javie;
- nauczyłem się pracować z API stron trzecich (Telegram Bot API);
- w praktyce zagłębiałem się w serializację, dużo pracowałem z JSON i biblioteką Jacksona (początkowo korzystałem z GSON, ale były z tym problemy);
- udoskonaliłem swoje umiejętności pracy z plikami, zapoznałem się z Javą NIO;
- nauczyłem się pracować z plikami konfiguracyjnymi .xml i przyzwyczaiłem się do logowania;
- poprawa biegłości w środowisku programistycznym (IDEA);
- nauczyłem się pracować z git i poznałem wartość gitignore;
- zdobył umiejętności w zakresie parsowania stron internetowych (biblioteka Jsoup);
- poznałem i zastosowałem kilka wzorców projektowych;
- rozwinął zmysł i chęć ulepszenia kodu (refactoring);
- Nauczyłam się znajdować rozwiązania w Internecie i nie bać się zadawać pytań, na które nie mogę znaleźć odpowiedzi.
GO TO FULL VERSION