JavaRush /Blog Java /Random-PL /Bot telegramowy - przypomnienie przez webHook w Javie lub...
Vladimir Popov
Poziom 41

Bot telegramowy - przypomnienie przez webHook w Javie lub powiedz nie kalendarzowi Google! Część 2

Opublikowano w grupie Random-PL
Druga część projektu - tutaj link do pierwszej: I tak klasa BotState : Aby nasz bot zrozumiał, czego się od niego w danym momencie oczekuje, np. usunięcie przypomnienia, musimy w jakiś sposób daj znać naszemu botowi, że wpisane i wysłane teraz numery należy traktować jako przypomnienie ID z listy i należy je usunąć. Dlatego też po kliknięciu przycisku „Usuń” bot przechodzi w stan BotState.ENTERNUMBEREVENT , jest to specjalnie utworzona klasa Enum ze stanami bota.
public enum BotState {
    ENTERDESCRIPTION,//the bot will wait for the description to be entered.
    START,
    MYEVENTS, //the bot show to user list events.
    ENTERNUMBEREVENT,//the bot will wait for the number of event to be entered.
    ENTERDATE, //the bot will wait for the date to be entered
    CREATE, //the bot run created event
    ENTERNUMBERFOREDIT, //the bot will wait for the number of event to be entered
    EDITDATE, //the bot will wait for the date to be entered
    EDITDESCRIPTION,//the bot will wait for the description to be entered
    EDITFREQ,//the bot will wait callbackquery
    ALLUSERS, // show all users
    ALLEVENTS, //show all events
    ENTERNUMBERUSER,//the bot will wait for the number of user to be entered.
    ENTERTIME,//the bot will wait for the hour to be entered.
    ONEVENT // state toggle
}
A teraz mamy wprowadzić cyfry – „ Wprowadź numer przypomnienia z listy ”. Po wprowadzeniu tego przejdą do żądanej metody przetwarzania. Oto nasz przełącznik stanu:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

    public void saveBotState(long userId, BotState botState) {
        botStateMap.put(userId, botState);
    }
}

</long,>
Zwykła mapa z identyfikatorem użytkownika i jego statusem. Pole int adminId jest dla mnie) Następnie logika metody handleUpdate sprawdzi, jaki to rodzaj wiadomości? Zapytanie zwrotne czy po prostu SMS? Jeśli jest to zwykły tekst, to przechodzimy do metody handleInputMessage , gdzie przetwarzamy przyciski menu głównego i jeśli zostały kliknięte, to ustawiamy pożądany stan, natomiast jeśli nie zostały kliknięte i jest to nieznany tekst, to ustawiamy stan z pamięci podręcznej, jeżeli jej tam nie ma to ustawiamy stan początkowy. Następnie tekst przechodzi do przetwarzania metody uchwytu z potrzebnym stanem. Przedstawiamy teraz logikę klasy MessageHandler , która odpowiada za przetwarzanie komunikatów w zależności od stanu bota:
public class MessageHandler {

    private final UserDAO userDAO;
    private final MenuService menuService;
    private final EventHandler eventHandler;
    private final BotStateCash botStateCash;
    private final EventCash eventCash;

    public MessageHandler(UserDAO userDAO, MenuService menuService, EventHandler eventHandler, BotStateCash botStateCash, EventCash eventCash) {
        this.userDAO = userDAO;
        this.menuService = menuService;
        this.eventHandler = eventHandler;
        this.botStateCash = botStateCash;
        this.eventCash = eventCash;
    }

    public BotApiMethod<!--?--> handle(Message message, BotState botState) {
        long userId = message.getFrom().getId();
        long chatId = message.getChatId();
        SendMessage sendMessage = new SendMessage();
        sendMessage.setChatId(String.valueOf(chatId));
        //if new user
        if (!userDAO.isExist(userId)) {
            return eventHandler.saveNewUser(message, userId, sendMessage);
        }
        //save state in to cache
        botStateCash.saveBotState(userId, botState);
        //if state =...
        switch (botState.name()) {
            case ("START"):
                return menuService.getMainMenuMessage(message.getChatId(),
                        "Воспользуйтесь главным меню", userId);
            case ("ENTERTIME"):
                //set time zone user. for correct sent event
                return eventHandler.enterLocalTimeUser(message);
            case ("MYEVENTS"):
                //list events of user
                return eventHandler.myEventHandler(userId);
            case ("ENTERNUMBEREVENT"):
                //remove event
                return eventHandler.removeEventHandler(message, userId);
            case ("ENTERDESCRIPTION"):
                //enter description for create event
                return eventHandler.enterDescriptionHandler(message, userId);
            case ("ENTERDATE"):
                //enter date for create event
                return eventHandler.enterDateHandler(message, userId);
            case ("CREATE"):
                //start create event, set state to next step
                botStateCash.saveBotState(userId, BotState.ENTERDESCRIPTION);
                //set new event to cache
                eventCash.saveEventCash(userId, new Event());
                sendMessage.setText("Введите описание события");
                return sendMessage;
            case ("ENTERNUMBERFOREDIT"):
                //show to user selected event
                return eventHandler.editHandler(message, userId);
            case ("EDITDESCRIPTION"):
                //save new description in database
                return eventHandler.editDescription(message);
            case ("EDITDATE"):
                //save new date in database
                return eventHandler.editDate(message);
            case ("ALLEVENTS"):
                //only admin
                return eventHandler.allEvents(userId);
            case ("ALLUSERS"):
                //only admin
                return eventHandler.allUsers(userId);
            case ("ONEVENT"):
                // on/off notification
                return eventHandler.onEvent(message);
            case ("ENTERNUMBERUSER"):
                //only admin
                return eventHandler.removeUserHandler(message, userId);
            default:
                throw new IllegalStateException("Unexpected value: " + botState);
        }
    }
}
W metodzie handle sprawdzamy status otrzymanej wiadomości i wysyłamy ją do obsługi zdarzenia – klasy EventHandler. Tutaj mamy dwie nowe klasy, MenuService i EventCash . MenuService – tutaj tworzymy wszystkie nasze menu. EventCash - podobnie jak BotStateCash, zapisze część naszego zdarzenia po wprowadzeniu danych, a po zakończeniu wprowadzania zapiszemy wydarzenie w bazie danych.
@Service
@Setter
@Getter
// used to save entered event data per session
public class EventCash {

    private final Map<long, event=""> eventMap = new HashMap<>();

    public void saveEventCash(long userId, Event event) {
        eventMap.put(userId, event);
    }
}
</long,>
Cóż, to znaczy. gdy tworzymy wydarzenie, w pamięci podręcznej tworzony jest nowy obiekt Event -eventCash.saveEventCash(userId, new Event()); Następnie wpisujemy opis wydarzenia i dodajemy go do pamięci podręcznej:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
Następnie wpisz numer:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
Klasa CallbackQueryHandler jest podobna do klasy MessageHandler , tyle że przetwarzamy w niej komunikaty wywołania zwrotnego. Nie ma sensu całkowicie analizować logiki pracy ze zdarzeniami - EventHandler , liter jest już za dużo, wynika to z nazw metod i komentarzy w kodzie. I nie widzę sensu przedstawiania tego całkowicie w tekście, jest ponad 300 linii. Oto link do zajęć na Githubie . To samo dotyczy klasy MenuService , w której tworzymy nasze menu. Możesz o nich szczegółowo przeczytać na stronie producenta biblioteki telegramów - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md Lub w książce referencyjnej Telegramu - https:// tlgrm.ru/docs/bots /api Teraz pozostaje najsmaczniejsza część. To jest klasa do obsługi komunikatów EventService :
@EnableScheduling
@Service
public class EventService {
    private final EventDAO eventDAO;
    private final EventCashDAO eventCashDAO;

    @Autowired
    public EventService(EventDAO eventDAO, EventCashDAO eventCashDAO) {
        this.eventDAO = eventDAO;
        this.eventCashDAO = eventCashDAO;
    }

    //start service in 0:00 every day
    @Scheduled(cron = "0 0 0 * * *")
    // @Scheduled(fixedRateString = "${eventservice.period}")
    private void eventService() {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int month = calendar.get(Calendar.MONTH);
        int year = calendar.get(Calendar.YEAR);

        //get event list is now date
        List<event> list = eventDAO.findAllEvent().stream().filter(event -> {
            if (event.getUser().isOn()) {
                EventFreq eventFreq = event.getFreq();

                //set user event time
                Calendar calendarUserTime = getDateUserTimeZone(event);

                int day1 = calendarUserTime.get(Calendar.DAY_OF_MONTH);
                int month1 = calendarUserTime.get(Calendar.MONTH);
                int year1 = calendarUserTime.get(Calendar.YEAR);
                switch (eventFreq.name()) {
                    case "TIME": //if one time - remove event
                        if (day == day1 && month == month1 && year == year1) {
                            eventDAO.remove(event);
                            return true;
                        }
                    case "EVERYDAY":
                        return true;
                    case "MONTH":
                        if (day == day1) return true;
                    case "YEAR":
                        if (day == day1 && month == month1) return true;
                    default: return false;
                }
            } else return false;
        }).collect(Collectors.toList());

        for (Event event : list) {
            //set user event time
            Calendar calendarUserTime = getDateUserTimeZone(event);
            int hour1 = calendarUserTime.get(Calendar.HOUR_OF_DAY);
            calendarUserTime.set(year, month, day, hour1, 0, 0);

            String description = event.getDescription();
            String userId = String.valueOf(event.getUser().getId());

            //save the event to the database in case the server reboots.
            EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
            eventCashDAO.save(eventCashEntity);

            //create a thread for the upcoming event with the launch at a specific time
            SendEvent sendEvent = new SendEvent();
            sendEvent.setSendMessage(new SendMessage(userId, description));
            sendEvent.setEventCashId(eventCashEntity.getId());

            new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime());
        }
    }

    private Calendar getDateUserTimeZone(Event event) {
        Calendar calendarUserTime = Calendar.getInstance();
        calendarUserTime.setTime(event.getDate());
        int timeZone = event.getUser().getTimeZone();

        //set correct event time with user timezone
        calendarUserTime.add(Calendar.HOUR_OF_DAY, -timeZone);
        return calendarUserTime;
    }
}

</event>
@EnableScheduling – włącz zaplanowaną pracę na wiosnę. @Scheduled(cron = "0 0 0 * * *") – konfigurujemy metodę tak, aby była uruchamiana każdego dnia o godzinie 00:00 Calendar.setTime(new Date()); - ustaw czas serwera. Listę przypomnień na dzisiaj otrzymujemy dzięki magii strumieni i lambdy. Przeglądamy otrzymaną listę, ustawiamy poprawny czas wysyłki CalendarUserTime i... Tutaj postanowiłem ominąć i uruchomić opóźnione w czasie procesy. Odpowiedzialna za to jest klasa Time w Javie . nowy Timer().schedule(nowe SimpleTask(sendEvent), kalendarzUserTime.getTime()); W tym celu musimy utworzyć wątek:
public class SendEvent extends Thread {


    private long eventCashId;
    private SendMessage sendMessage;

    public SendEvent() {
    }

    @SneakyThrows
    @Override
    public void run() {
        TelegramBot telegramBot = ApplicationContextProvider.getApplicationContext().getBean(TelegramBot.class);
        EventCashDAO eventCashDAO = ApplicationContextProvider.getApplicationContext().getBean(EventCashDAO.class);
        telegramBot.execute(sendMessage);
        //if event it worked, need to remove it from the database of unresolved events
        eventCashDAO.delete(eventCashId);
    }
}
i implementacja TimerTask
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

    public SimpleTask(SendEvent sendEvent) {
        this.sendEvent = sendEvent;
    }

    @Override
    public void run() {
        sendEvent.start();
    }
}
Tak, doskonale rozumiem, że można przeglądać bazę danych co 20 minut i wysyłać wiadomości, ale o tym pisałem wszystko na samym początku)) Tutaj także spotykamy się z nędzą Heroku nr 1. W planie darmowym otrzymujesz około 550 dino, czyli mniej więcej tyle godzin działania Twojej aplikacji w miesiącu. To nie wystarczy na cały miesiąc działania aplikacji, ale jeśli podłączysz kartę, otrzymasz kolejne 450 dino, co wystarczy na Twoje oczy. Jeśli martwisz się o kartę, możesz podpiąć pustą, ale upewnij się, że zawiera ona 0,6 $... To jest kwota weryfikacyjna, wystarczy, że będzie na koncie. Nie ma żadnych ukrytych opłat, chyba że samodzielnie zmienisz taryfę. W planie darmowym jest jeszcze jeden mały problem, nazwijmy go nr 1a.. Ciągle restartują serwery lub po prostu wysyłają polecenie ponownego uruchomienia aplikacji, ogólnie restartuje się codziennie gdzieś o północy czasu moskiewskiego, a czasem innym razem. Z tego powodu wszystkie nasze procesy w pamięci są usuwane. Aby rozwiązać ten problem, wymyśliłem tabelę EventCash. Przed wysłaniem zdarzenia zapisywane są w osobnej tabeli:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
A po wysłaniu usuwane są:
@Override
public void run() {
    TelegramBot telegramBot = ApplicationContextProvider.getApplicationContext().getBean(TelegramBot.class);
    EventCashDAO eventCashDAO = ApplicationContextProvider.getApplicationContext().getBean(EventCashDAO.class);
    telegramBot.execute(sendMessage);
    //if event it worked, need to remove it from the database of unresolved events
    eventCashDAO.delete(eventCashId);
}
ApplicationContextProvider to specjalna klasa umożliwiająca uzyskanie kontekstu na bieżąco:
@Component
//wrapper to receive Beans
public class ApplicationContextProvider implements ApplicationContextAware {

    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext ac)
            throws BeansException {
        context = ac;
    }
}
Aby sprawdzić nieprzetworzone zdarzenia, stworzyłem specjalną usługę, która ma metodę oznaczoną @PostConstruct - uruchamia się po każdym uruchomieniu aplikacji. Pobiera wszystkie nieprzetworzone zdarzenia z bazy danych i zwraca je do pamięci. Oto paskudny Heroku dla Ciebie!
@Component
public class SendEventFromCache {

    private final EventCashDAO eventCashDAO;
    private final TelegramBot telegramBot;

    @Value("${telegrambot.adminId}")
    private int admin_id;

    @Autowired
    public SendEventFromCache(EventCashDAO eventCashDAO, TelegramBot telegramBot) {
        this.eventCashDAO = eventCashDAO;
        this.telegramBot = telegramBot;
    }

    @PostConstruct
    @SneakyThrows
    //after every restart app  - check unspent events
    private void afterStart() {
        List<eventcashentity> list = eventCashDAO.findAllEventCash();

        SendMessage sendMessage = new SendMessage();
        sendMessage.setChatId(String.valueOf(admin_id));
        sendMessage.setText("Произошла перезагрузка!");
        telegramBot.execute(sendMessage);

        if (!list.isEmpty()) {
            for (EventCashEntity eventCashEntity : list) {
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(eventCashEntity.getDate());
                SendEvent sendEvent = new SendEvent();
                sendEvent.setSendMessage(new SendMessage(String.valueOf(eventCashEntity.getUserId()), eventCashEntity.getDescription()));
                sendEvent.setEventCashId(eventCashEntity.getId());
                new Timer().schedule(new SimpleTask(sendEvent), calendar.getTime());
            }
        }
    }
}
</eventcashentity>
Nasza aplikacja jest już gotowa, przyszedł czas na zdobycie adresu Heroku dla aplikacji i bazy danych. Twoja aplikacja musi zostać opublikowana na Githubie!!! Przejdź do Heroku.com Kliknij Utwórz nową aplikację , wprowadź nazwę aplikacji, wybierz Europę, utwórz aplikację . To wszystko, miejsce na aplikację gotowe. Jeśli klikniesz Otwórz aplikację, przeglądarka przekieruje Cię na adres Twojej aplikacji, jest to adres Twojego webhooka - https://twoja_nazwa.herokuapp.com/ Zarejestruj go w telegramie, a w ustawieniach aplikacji.property zmień telegrambota. webHookPath=https: //telegrambotsimpl.herokuapp.com/ na serwerze server.port=5000 można usunąć lub skomentować. Teraz połączmy bazę danych. Przejdź do zakładki Zasoby w Heroku, kliknij: Bot telegramowy - przypomnienie przez webHook w Javie lub powiedz nie kalendarzowi Google!  Część 2: - 1 Znajdź Heroku Postgres , kliknij instaluj : Zostaniesz przekierowany na stronę konta bazy danych. Znajdź go tam w Ustawieniach/ Bot telegramowy - przypomnienie przez webHook w Javie lub powiedz nie kalendarzowi Google!  Część 2: - 2 Będą tam wszystkie niezbędne dane z Twojej bazy danych. W application.properties wszystko powinno teraz wyglądać tak:
#server.port=5000

telegrambot.userName=@calendar_event_bot
telegrambot.botToken=1731265488:AAFDjUSk3vu5SFfgdfh556gOOFmuml7SqEjwrmnEF5Ak
telegrambot.webHookPath=https://telegrambotsimpl.herokuapp.com/
#telegrambot.webHookPath=https://f5d6beeb7b93.ngrok.io


telegrambot.adminId=39376213

eventservice.period =600000

#spring.datasource.driver-class-name=org.postgresql.Driver
#spring.datasource.url=jdbc:postgresql://localhost:5432/telegramUsers
#spring.datasource.username=postgres
#spring.datasource.password=password

spring.datasource.url=jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org.postgresql.ssl.NonValidatingFactory
spring.datasource.username=ulmbeymwyvsxa
spring.datasource.password=4c7646c69dbgeacbk98fa96e8daa6d9b1bl4894e67f3f3ddd6a27fe7b0537fd
Zamień dane ze swojego konta na swoje: W polu jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode=require&sslfactory=org. postgresql.ssl .NonValidatingFactory należy zastąpić pogrubioną czcionką odpowiednimi danymi z konta (Host, Baza danych) Pola nazwy użytkownika i hasła nie są trudne do odgadnięcia. Teraz musimy utworzyć tabele w bazie danych, zrobiłem to z IDEA. Nasz skrypt będzie przydatny przy tworzeniu bazy danych. Bazę danych dodajemy tak jak napisano powyżej: Pobieramy z konta Bot telegramowy - przypomnienie przez webHook w Javie lub powiedz nie kalendarzowi Google!  Część 2: - 3 pole Host, Użytkownik, Hasło, Baza Danych . Pole URl to nasze pole spring.datasource.url aż do znaku zapytania. Przechodzimy do zakładki Zaawansowane , powinno wyglądać tak: Bot telegramowy - przypomnienie przez webHook w Javie lub powiedz nie kalendarzowi Google!  Część 2: - 4 Jeśli wszystko zrobiłeś poprawnie, to po kliknięciu na test pojawi się zielony znacznik wyboru. Kliknij OK. Kliknij prawym przyciskiem myszy naszą bazę danych i wybierz opcję Skocz do konsoli zapytań . Skopiuj tam nasz skrypt i kliknij Wykonaj . Należy utworzyć bazę danych. 10 000 linii jest dostępnych dla Ciebie za darmo! Wszystko jest gotowe do wdrożenia. Przejdź do naszej aplikacji na Heroku w sekcji Wdróż. Wybierz tam sekcję Github: Bot telegramowy - przypomnienie przez webHook w Javie lub powiedz nie kalendarzowi Google!  Część 2: - 5 Połącz swoje repozytorium z Heroku. Teraz twoje gałęzie będą widoczne. Nie zapomnij przesłać najnowszych zmian do pliku .properties. Poniżej wybierz gałąź, która zostanie pobrana i kliknij Wdróż gałąź . Jeśli wszystko zostało wykonane poprawnie, zostaniesz powiadomiony, że aplikacja została pomyślnie wdrożona. Nie zapomnij włączyć opcji Automatyczne wdrażanie z .., aby Twoja aplikacja uruchomiła się automatycznie. Nawiasem mówiąc, kiedy wyślesz zmiany do GitHuba, Heroku automatycznie zrestartuje aplikację. Uważaj na to, utwórz osobny wątek na temat znęcania się, a głównego używaj tylko dla działającej aplikacji. Teraz Taniość nr 2! Jest to dobrze znana wada darmowego planu Heroku. Jeżeli nie ma żadnych przychodzących wiadomości, aplikacja przechodzi w stan gotowości, a po odebraniu wiadomości jej uruchomienie zajmuje dość dużo czasu, co nie jest przyjemne. Jest na to proste rozwiązanie – https://uptimerobot.com/ I nie, gadżety Google ping nie pomogą, nawet nie wiem, skąd wzięła się ta informacja, wpisałem to pytanie w Google i od około 10 lat ten temat na pewno nie zadziałał, jeśli w ogóle zadziałał. Aplikacja ta będzie wysyłać żądania HEAD na podany przez Ciebie adres w ustawionym przez Ciebie czasie, a w przypadku braku odpowiedzi wyśle ​​wiadomość e-mailem. Nie będzie ci trudno to rozgryźć, nie ma wystarczającej liczby przycisków, aby się pomylić)) Gratulacje! Jeśli o niczym nie zapomniałem i byłeś uważny, masz własną aplikację, która działa za darmo i nigdy się nie zawiesza. Otwiera się przed tobą szansa na znęcanie się i eksperymentowanie. W każdym razie jestem gotowy odpowiedzieć na pytania i przyjąć każdą krytykę! Kod: https://github.com/papoff8295/webHookBotForHabr Wykorzystane materiały: https://tlgrm.ru/docs/bots/api - o botach. https://en.wikibooks.org/wiki/Java_Persistence - o relacjach w bazach danych. https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-Java - Klasa czasu i TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – jak opublikować kod na Githubie https://github.com/rubenlagus/TelegramBots - biblioteka telegramów i wiele przydatnych informacji na jej temat.
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION