@Override
public void onUpdateReceived(Update update) {
UpdatesReceiver.handleUpdates(update);
}
これをハンドラー (独自の 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(), "Я могу принимать только текстовые messages!");
}
}
UpdatesReceiver は、更新の種類に応じて、別の特殊なハンドラー (TextMessageHandler または CallbackQueryHandler) に制御を移す中央ハンドラーであり、そのコンストラクターに更新をチェーンのさらに下流に渡します。ボットを使用する場合、更新は最も重要であり、更新を忘れることはできません。更新に保存されている情報を利用して、どのユーザーとどのチャットに応答を送信すべきかを特定するからです。ユーザーへの応答を生成するために、別のクラスを作成しました。通常のテキストメッセージ、インラインキーボード付きメッセージ、画像付きメッセージ、返信キーボード付きメッセージを送信できます。インライン キーボードは次のようになります。 ボタンが定義されており、ボタンをクリックすると、ユーザーはサーバーにコールバックを送信します。このコールバックは、通常のメッセージとほぼ同じ方法で処理できます。それを「維持」するには、独自のハンドラーが必要です。各ボタンのアクションを設定し、Update オブジェクトに書き込みます。それらの。「Cost」ボタンにはコールバックの説明「/price」を設定します。これは後でアップデートから取得できます。次に、別のクラスで、このコールバックをすでに処理できます。
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;
...
Reply キーボードは次のようになります。 そして本質的には、ユーザーの入力を置き換えます。「ライブラリ」ボタンをクリックすると、すぐに「ライブラリ」メッセージがボットに送信されます。キーボードの種類ごとに、Builder パターンであるinlineとReplyを実装する独自のクラスを作成しました。その結果、要件に応じて必要なキーボードを基本的に「描く」ことができます。キーボードは異なる場合がありますが、原理は同じなので、これは非常に便利です。インライン キーボードを使用してメッセージを送信する直感的な方法は次のとおりです。
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());
}
}
ボットに厳密な機能を与えるために、スラッシュ文字を使用した特別なコマンド (/library、/help、/game など) が考案されました。そうしないと、ユーザーが書き込む可能性のあるガーベジを処理する必要があります。実際、これは 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());
}
...
したがって、ボットに送信するコマンドに応じて、特別なハンドラーが作業に組み込まれます。さらに進んで、パーサーとライブラリの動作を見てみましょう。Google Play ストアのゲームへのリンクをボットに送信すると、特別なハンドラーが自動的に動作します。これに応じて、ユーザーは次の形式でゲームに関する情報を受け取ります。 同時に、ゲームをボットのライブラリに追加しようとするメソッドが呼び出されます (最初にローカル マップに、次に -> json ファイルに) )。ゲームがすでにライブラリにある場合は (通常のハッシュマップと同様に) チェックが実行され、フィールド データ (バージョン番号が変更されているなど) がある場合は、ライブラリ内のゲームが上書きされます。変更が検出されない場合、エントリは作成されません。ライブラリにゲームがまったくなかった場合は、最初にローカル マップ ( tykなどのオブジェクト) に書き込まれ、次に json ファイルに書き込まれます。これは、サーバー上のアプリケーションが予期せず閉じられた場合にデータが失われるためです。失われますが、ファイルを使用していつでも読み取ることができます。実際には、プログラムが開始されると、ライブラリは常に静的ブロックのファイルから最初にロードされます。
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("[Ошибка при чтении/записи file] {}", e.getMessage());
}
}
ここではさらに、ファイルから一時マップにデータを読み取る必要があります。ファイル内のゲームを検索するときに大文字と小文字を区別しないようにするために、そのデータが完全なマップに「コピー」されます (tITan QuEST を記述することで、ボットは引き続き検索します)ライブラリにあるゲーム Titan Quest)。別の解決策を見つけることはできませんでした。これらは、Jackson を使用した逆シリアル化の機能です。したがって、リンクを要求するたびに、可能であればゲームがライブラリに追加され、それによってライブラリが拡張されます。特定のゲームに関する詳細情報は、コマンド/libraryGame_Name を使用して取得できます。特定のパラメーター (現在のバージョンなど) とすべてのパラメーターの両方を一度に確認できます。これは、前に説明したインライン キーボードを使用して実装されます。仕事中も、ここで得たスキルを応用しながら問題を解決しました。たとえば、ライブラリにあるランダムなゲームの名前のリスト (このオプションは /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);
}
ボットはリンクをどのように処理しますか? それらが Google Play に属しているかどうか (ホスト、プロトコル、ポート) を確認します。
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;
}
}
すべてが正常であれば、ボットは Jsoup ライブラリを使用したリンク経由で接続します。これにより、ページの HTML コードを取得できるようになり、さらに分析と解析が行われます。間違ったリンクや有害なリンクを使用してボットをだますことはできません。
if (GooglePlayCorrectURL.isLinkValid(link)){
if (!link.getPath().contains("apps")){
throw new InvalidGooglePlayLinkException("К сожалению, бот работает исключительно с играми. Введите другую ссылку.");
}
URL = forceToRusLocalization(URL);
document = Jsoup.connect(URL).get();
}
else {
throw new NotGooglePlayLinkException();
}
...
ここでは、地域設定に関する問題を解決する必要がありました。ボットはヨーロッパにあるサーバーから Google Play ストアに接続するため、Google Play ストアのページは適切な言語で開きます。私は、ページのロシア語版に強制的に「リダイレクト」する松葉杖を書かなければなりませんでした(結局のところ、このプロジェクトは私たちの視聴者を対象としたものでした)。これを行うには、リンクの最後に、 Google Play サーバーへの GET リクエストにパラメータ hl: &hl=ruを慎重に追加する必要があります。
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;
}
接続が成功すると、すぐに分析および解析できる HTML ドキュメントを受け取りますが、これについてはこの記事の範囲を超えています。パーサーコードはここにあります。パーサー自体が必要な情報を取得し、ゲームのオブジェクトを作成します。このオブジェクトは、必要に応じて後でライブラリに追加されます。<h2>要約</h2>ボットは、特定の機能を含むいくつかのコマンドをサポートしています。ユーザーからメッセージを受信し、それをコマンドと照合します。リンクまたは /game + link コマンドの場合は、そのリンクが Google Play に属しているかどうかを確認します。リンクが正しければ、Jsoup 経由で接続し、HTML ドキュメントを受信します。この文書は、書かれたパーサーに基づいて分析されます。ゲームに関する必要な情報がドキュメントから抽出され、ゲームが含まれるオブジェクトにこのデータが埋め込まれます。次に、ゲームを含むオブジェクトがローカル ストレージに配置され (ゲームがまだ存在しない場合)、データの損失を避けるためにすぐにファイルに書き込まれます。ライブラリに記録されているゲーム (ゲームの名前はマップのキー、ゲームを含むオブジェクトはマップの値) は、コマンド /library Game_name を使用して取得できます。指定されたゲームがボットのライブラリで見つかった場合、ユーザーにはインライン キーボードが返され、これを使用してゲームに関する情報を取得できます。ゲームが見つからない場合は、名前のスペルが正しいことを確認するか (大文字と小文字を除いて、Google Play ストアのゲーム名と完全に一致する必要があります)、ボットを送信してゲームをライブラリに追加する必要があります。ゲームへのリンク。私は Heroku にボットをデプロイしました。将来、独自のボットを作成して Heroku で無料でホストすることを計画している人のために、遭遇する可能性のある問題を解決するための推奨事項をいくつか示します (私自身がそのような問題に遭遇したため)。残念ながら、Heraku の性質上、ボット ライブラリは 24 時間ごとに常に「リセット」されます。私のプランでは Heroku サーバーへのファイルの保存はサポートされていないため、単純に Github からゲーム ファイルをプルします。解決策はいくつかありました。データベースを使用するか、このファイルをゲームとともに保存する別のサーバーを探すかです。基本的にボットはそれほど役に立たないため、今のところは何もしないことにしました。むしろ、完全な経験を積むためにそれが必要でしたし、基本的にはそれが私が達成したことです。Heroku の推奨事項:
-
ロシアにお住まいの場合は、VPN を使用して Heroku に登録する必要がある可能性があります。
-
プロジェクトのルートに、Procfile という拡張子のないファイルを置く必要があります。その内容は次のようになります: https://github.com/miroha/Telegram-Bot/blob/master/Procfile
-
pom.xml で、例に従って次の行を追加します。 mainClass タグでは、メイン メソッド bot.BotApplication (BotApplication クラスが bot フォルダーにある場合) を含むクラスへのパスを示します。
-
mvn package コマンドなどを使用してプロジェクトをビルドしないでください。Heraku がすべてをアセンブルします。
-
たとえば次のように、gitignore をプロジェクトに追加することをお勧めします。
# Log file *.log # Compiled resources target # Tests test # IDEA files .idea *.iml
-
実際にプロジェクトを github にアップロードし、リポジトリを Heroku に接続します (または、他の方法を使用します。私の記憶が間違っていなければ、3 つあります)。
-
ダウンロードが成功した場合 (「ビルドに成功しました」)、必ず Dynos の構成に進んでください。
スライダーを切り替えて、オンの位置にあることを確認します(これを行わなかったためにボットが機能せず、数日間頭を悩ませ、多くの不要な動きをしました) )。
-
Github 上のボット トークンを非表示にします。これを行うには、環境変数からトークンを取得する必要があります。
public class Bot extends TelegramLongPollingBot { private static final String BOT_TOKEN = System.getenv("TOKEN"); @Override public String getBotToken() { return BOT_TOKEN; } ... }
そして、ボットをデプロイした後、設定タブの Heroku ダッシュボードでこの変数を設定します (TOKEN の右側に VALUE フィールドがあり、そこにボットのトークンをコピーします)。
- Java で書かれた完全に動作するプロジェクトを受け取りました。
- サードパーティ API (Telegram Bot API) の使い方を学びました。
- 実際には、シリアル化をさらに深く掘り下げ、JSON と Jackson ライブラリをよく使いました (最初は GSON を使用していましたが、それには問題がありました)。
- ファイルを扱う際のスキルを強化し、Java NIO について知りました。
- 構成 .xml ファイルの操作方法を学び、ログ記録に慣れました。
- 開発環境 (IDEA) の習熟度が向上しました。
- git の使い方を学び、gitignore の価値を学びました。
- Web ページ解析 (Jsoup ライブラリ) のスキルを習得しました。
- いくつかのデザインパターンを学び、使用しました。
- コード (リファクタリング) を改善する感覚と欲求を養いました。
- オンラインで解決策を見つけること、答えが見つからない質問を恥ずかしがらないことを学びました。
GO TO FULL VERSION