プロジェクトの 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 を 見つけて、[インストール]をクリックします。データベース アカウント ページにリダイレクトされます。設定でそれを見つけてください/ データベースから必要なデータがすべてあります。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 を太字で、アカウント (ホスト、データベース) の対応するデータに置き換える必要があります。ユーザー名、パスワードのフィールドは推測するのが難しくありません。次に、データベースにテーブルを作成する必要があります。これは IDEA から行いました。私たちのスクリプトはデータベースの作成に役立ちます。上記のようにデータベースを追加します。 アカウントから ホスト、ユーザー、パスワード、データベースのフィールドを取得します。URlフィールドは、疑問符までの spring.datasource.url フィールドです。「詳細」タブに移動します。次のようになります。 すべてが正しく行われた場合、「テスト」をクリックすると、緑色のチェックマークが表示されます。「OK」をクリックします。データベースを右クリックし、[クエリ コンソールにジャンプ]を選択します。そこにスクリプトをコピーし、「実行」をクリックします。データベースが作成されるはずです。10,000回線が無料で利用可能!デプロイの準備はすべて完了しました。「デプロイ」セクションで Heroku 上のアプリケーションに移動します。そこで Github セクションを選択します: リポジトリを 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 – 電報ライブラリとそれに関する多くの役立つ情報。
GO TO FULL VERSION