JavaRush /Java Blog /Random-JA /Telegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダ...
Vladimir Popov
レベル 41

Telegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダーに「ノー」と伝えます。パート2

Random-JA グループに公開済み
プロジェクトの 2 番目の部分 - ここに最初の部分へのリンクがあります: そして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 と EventCash という 2 つの新しいクラスがあります。 MenuService – ここですべてのメニューを作成します。 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クラスにも当てはまります。詳細については、電報ライブラリの製造元の Web サイト - 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 に実行されるようにメソッドを設定します - サーバー時間を設定します。ストリームとラムダの魔法を通じて、今日のリマインダーのリストを取得します。受信したリストを確認し、正しい送信時刻を設定します。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 分ごとにデータベースを調べてメッセージを送信できることはよく理解していますが、これについては最初にすべて書きました)) ここで、Heraku No. 1 の悲惨な状況にも遭遇します。無料プランでは、約 550 ディノが与えられます。これは、アプリケーションの 1 か月あたりの動作時間に相当します。これは、アプリケーションを丸 1 か月間操作するには十分ではありませんが、カードをリンクすると、さらに 450 恐竜が与えられるので、目には十分です。カードが心配な場合は、空のカードをリンクできますが、そのカードに 0.6 ドルが含まれていることを確認してください。これは確認用の金額であり、アカウントにあれば十分です。自分で料金を変更しない限り、隠れた料金は発生しません。無料プランには、もう 1 つの小さな問題があります。それを 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] を選択して、[create app] を選択します。以上で、アプリケーションを配置する場所の準備が整いました。[アプリを開く]をクリックすると、ブラウザーはアプリケーションのアドレスにリダイレクトします。これは Webhook アドレスです - https://your_name.herokuapp.com/ これをテレグラムに登録し、application.propertyの設定でテレグラムボットを変更します。 webHookPath=https://telegrambotsimpl.herokuapp.com/を、 server.port=5000に 削除するか、コメントアウトすることができます。それでは、データベースに接続してみましょう。Heroku の [リソース]タブに移動し、次をクリックします。 そこでHeroku Postgres をTelegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダーに「ノー」と伝えます。 パート 2: - 1 見つけて、[インストール]をクリックします。データベース アカウント ページにリダイレクトされます。設定でそれを見つけてください/ データベースから必要なデータがすべてあります。application.propertiesでは、すべてが次のようになります。 Telegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダーに「ノー」と伝えます。 パート 2: - 2
#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 を太字で、アカウント (ホスト、データベース) の対応するデータに置き換える必要があります。ユーザー名、パスワードのフィールドは推測するのが難しくありません。次に、データベースにテーブルを作成する必要があります。これは IDEA から行いました。私たちのスクリプトはデータベースの作成に役立ちます。上記のようにデータベースを追加します。 アカウントからTelegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダーに「ノー」と伝えます。 パート 2: - 3 ホスト、ユーザー、パスワード、データベースのフィールドを取得します。URlフィールドは、疑問符までの spring.datasource.url フィールドです。「詳細」タブに移動します。次のようになります。 Telegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダーに「ノー」と伝えます。 パート 2: - 4 すべてが正しく行われた場合、「テスト」をクリックすると、緑色のチェックマークが表示されます。「OK」をクリックします。データベースを右クリックし、[クエリ コンソールにジャンプ]を選択します。そこにスクリプトをコピーし、「実行」をクリックします。データベースが作成されるはずです。10,000回線が無料で利用可能!デプロイの準備はすべて完了しました。「デプロイ」セクションで Heroku 上のアプリケーションに移動します。そこで Github セクションを選択します: Telegram ボット - Java の WebHook 経由でリマインダーを送信するか、Google カレンダーに「ノー」と伝えます。 パート 2: - 5 リポジトリを Heroku にリンクします。これでブランチが表示されるようになります。最新の変更を .properties にプッシュすることを忘れないでください。以下で、ダウンロードするブランチを選択し、[ブランチのデプロイ]をクリックします。すべてが正しく行われると、アプリケーションが正常にデプロイされたことが通知されます。アプリケーションが自動的に開始されるように、「..からの自動デプロイ」を有効にすることを忘れないでください。ちなみに、変更を GitHub にプッシュすると、Heraku はアプリケーションを自動的に再起動します。これに注意して、いじめ用に別のスレッドを作成し、メインのスレッドは動作するアプリケーションにのみ使用してください。今なら安さNo.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