JavaRush /Java Blog /Random-TW /Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不!第2部分
Vladimir Popov
等級 41

Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不!第2部分

在 Random-TW 群組發布
這個專案的第二部分 - 這是第一部分的連結:因此BotState類:為了讓我們的機器人了解在某個時間點對其的期望,例如刪除提醒,我們需要以某種方式讓我們的機器人知道現在輸入和發送的號碼應該被視為清單中的提醒ID,並且應該被刪除。因此,點擊「刪除」按鈕後,機器人會進入BotState.ENTERNUMBEREVENT狀態,這是一個專門建立的具有機器人狀態的 Enum 類別。
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
}
現在我們需要輸入數字 - “從清單中輸入提醒號碼。” 輸入後,他們將轉到所需的處理方法。這是我們的狀態開關:
public class BotStateCash {
    private final Map<long, botstate=""> botStateMap = new HashMap<>();

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

</long,>
帶有使用者 ID 及其狀態的常規地圖。int adminId欄位是給我的)接下來, handleUpdate方法的邏輯將檢查這是什麼類型的消息?回調查詢還是只是文字?如果這是常規文本,那麼我們轉到handleInputMessage方法,在該方法中我們處理主選單按鈕,如果單擊了它們,那麼我們設定所需的狀態,但如果沒有單擊它們並且這是不熟悉的文本,那麼我們從快取中設定狀態,如果不存在,則設定起始狀態。然後正文進入處理具有我們需要的狀態的句柄方法。現在我們介紹MessageHandler類別的邏輯,它負責根據機器人的狀態處理訊息:
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);
        }
    }
}
在handle方法中,我們檢查收到的訊息的狀態並將其傳送到事件處理程序-EventHandler類別。 這裡我們有兩個新類別:MenuService 和 EventCashMenuService – 在這裡我們建立所有選單。 EventCash - 與 BotStateCash 類似它會在輸入後保存我們的部分事件,當輸入完成時,我們會將事件保存在資料庫中。
@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,>
嗯,就是這樣。當我們建立一個事件時,會在快取中建立一個新的Event物件 -eventCash.saveEventCash(userId, new Event()); 然後我們輸入事件的描述並將其添加到快取中:
Event event = eventCash.getEventMap().get(userId);
event.setDescription(description);
//save to cache
eventCash.saveEventCash(userId, event);
然後輸入號碼:
Event event = eventCash.getEventMap().get(userId);
event.setDate(date);
//save data to cache
eventCash.saveEventCash(userId, event);
CallbackQueryHandler 類別與MessageHandler 類似,只是我們在那裡處理回呼查詢訊息。完全分析事件處理邏輯 - EventHandler是沒有意義的,字母已經太多了,從程式碼中的方法名稱和註解中可以清楚地看出。而且我看不出將其完全用文字來表達的意義,有 300 多行。這是Github上該課程的連結。對於我們創建菜單的MenuService類別也是如此。您可以在 Telegram 庫製造商的網站上詳細了解它們 - https://github.com/rubenlagus/TelegramBots/blob/master/TelegramBots.wiki/FAQ.md 或在 Telegram 參考書中 - https:// tlgrm. ru/docs/bots /api 現在我們剩下最美味的部分了。這是用於處理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 – 在 Spring 中啟用計劃工作。 @Scheduled(cron = "0 0 0 * * *") – 我們將方法配置為每天 0:00 執行 calendar.setTime(new Date()); - 設定伺服器時間。透過串流和 lambda 的魔力,我們獲得了今天的提醒清單。我們檢查收到的列表,設定正確的發送時間calendarUserTime,然後...這是我決定躲避並及時啟動延遲進程的地方。java 中的Time類別負責此工作。 new Timer().schedule(new SimpleTask(sendEvent), calendarUserTime.getTime()); 為此我們需要創建一個線程:
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);
    }
}
以及TimerTask 的實現
public class SimpleTask extends TimerTask {
    private final SendEvent sendEvent;

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

    @Override
    public void run() {
        sendEvent.start();
    }
}
是的,我完全理解你可以每 20 分鐘檢查一次資料庫並發送訊息,但我在一開始就寫了有關此的所有內容))在這裡我們也遇到了 Heroku No.1 的痛苦。在免費計劃中,您將獲得大約 550 個恐龍,這相當於您的應用程式每月運行的時間。這對於應用程式運行一整個月來說還不夠,但是如果你連結一張卡,你會得到另外 450 恐龍,這足夠你的眼睛了。如果您擔心該卡,您可以連結一張空卡,但請確保其中包含 0.6 美元...這是驗證金額,只需在帳戶中即可。除非您自己更改費率,否則沒有隱藏費用。在免費計劃中,還有一個小問題,讓我們稱之為No.1a..他們不斷地重新啟動伺服器,或者只是發送一個命令來重新啟動應用程序,一般來說,它每天都會在莫斯科時間午夜的某個地方重新啟動,有時會重新啟動。在其他時間。由此,我們記憶體中的所有程序都被刪除。為了解決這個問題,我想出了EventCash表。在發送之前,事件被保存在一個單獨的表中:
EventCashEntity eventCashEntity = EventCashEntity.eventTo(calendarUserTime.getTime(), event.getDescription(), event.getUser().getId());
eventCashDAO.save(eventCashEntity);
發送後,以下內容將被刪除:
@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是一個用於動態取得上下文的特殊類別:
@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;
    }
}
為了檢查未處理的事件,我創建了一個特殊的服務,其中有一個標記為@PostConstruct 的方法- 它在每個應用程式啟動後運行。它從資料庫中獲取所有未處理的事件並將它們返回到記憶體中。這是給你的一個討厭的 Heroku!
@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>
我們的應用程式已準備就緒,現在是時候獲取應用程式和資料庫的 Heroku 位址了。你的應用程式必鬚髮佈在Github上!!!前往 Heroku.com 點擊Create New App,輸入您的應用程式名稱,選擇Europe,建立應用程式。就這樣,應用程式的地方就準備好了。如果您點擊打開應用程序,瀏覽器會將您重定向到您的應用程式的地址,這是您的 webhook 地址 - https://your_name.herokuapp.com/ 在 telegram 中註冊它,並在application.property的設定中更改telegrambot。webHookPath=https://telegrambotsimpl.herokuapp.com/到你的 server.port=5000 可以刪除或註解掉。現在讓我們連接資料庫。前往Heroku 上的 資源Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不! 第 2 部分:- 1標籤,點擊:在那裡 尋找Heroku Postgres,點擊安裝:您將被重定向到您的資料庫帳戶頁面。在“設定/”中找到它, Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不! 第 2 部分:- 2 其中將包含資料庫中的所有必要資料。在application.properties中,一切現在應該是這樣的:
#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
將您帳戶中的資料替換為您的帳戶中的資料:在欄位jdbc:postgresql:ec2-54-247-158-179.eu-west-1.compute.amazonaws.com:5432/d2um26le5notq?ssl=true&sslmode =require&sslfactory=org 中。postgresql.ssl .NonValidatingFactory 需要用粗體字替換為帳戶(Host、Database)對應的數據,其中的用戶名、密碼字段不難猜測。現在我們需要在資料庫中建立表,我是在 IDEA 中完成的。我們的腳本對於建立資料庫非常有用。我們按照上面的方式新增資料庫: 我們從帳戶中取得Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不! 第 2 部分:- 3 主機、使用者、密碼、資料庫欄位。URl字段是我們的 spring.datasource.url 字段,直到問號。我們進入“高級”選項卡,它應該是這樣的: Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不! 第 2 部分:- 4 如果您所做的一切正確,那麼單擊“測試”後,將會有一個綠色的複選標記。按一下“確定”。右鍵單擊我們的資料庫並選擇跳到查詢控制台。將我們的腳本複製到那裡並點擊執行。應創建資料庫。10,000條線路免費供您使用!一切準備就緒,可以部署了。前往 Heroku 上的「部署」部分中的應用程式。選擇此處的 Github 部分: Telegram 機器人 - 透過 Java 中的 webHook 提醒或對 Google 日曆說不! 第 2 部分:- 5 將您的儲存庫連結到 Heroku。現在你的分支將可見。不要忘記將最新變更推送到 .properties。在下面,選擇要下載的分支,然後按一下部署分支。如果一切都正確完成,您將收到應用程式已成功部署的通知。不要忘記從 .. 啟用自動部署,以便您的應用程式自動啟動。順便說一句,當您將變更推送到 GitHub 時,Heroku 會自動重新啟動應用程式。請小心這一點,創建一個單獨的執行緒用於霸凌,並僅將主執行緒用於工作應用程式。現在便宜#2!這是 Heroku 免費方案眾所周知的缺點。如果沒有傳入訊息,應用程式將進入待機模式,並且在收到訊息後將需要相當長的時間才能啟動,這並不令人愉快。有一個簡單的解決方案 - https://uptimerobot.com/ 不,Google ping 小工具不會有幫助,我甚至不知道這些資訊來自哪裡,我用 Google 搜尋了這個問題,大約 10 年來,這個主題一直沒有確定的作用,如果它真的有效的話。此應用程式將在您設定的時間內向您指定的地址發送 HEAD 請求,如果沒有回應,則透過電子郵件發送訊息。對你來說,弄清楚它並不困難,沒有足夠的按鈕讓你感到困惑))恭喜你!如果我沒有忘記任何事情並且您很專心,那麼您就擁有了自己的應用程序,可以免費運行並且永遠不會崩潰。霸凌和實驗的機會就擺在你面前。無論如何,我願意回答問題並接受任何批評!程式碼:https://github.com/papoff8295/webHookBotForHabr 使用的資料:https://tlgrm.ru/docs/bots/api - 關於機器人。https://en.wikibooks.org/wiki/Java_Persistence - 關於資料庫中的關係。https://stackoverflow.com/questions/11432498/how-to-call-a-thread-to-run-on-specific-time-in-java - 時間類別和 TimerTask https://www.youtube.com/ watch?v=CUDgSbaYGx4 – 如何在Github 上發布程式碼https://github.com/rubenlagus/TelegramBots – 電報庫以及許多有關它的有用資訊。
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION