AndroidデバイスでTelegramボットを起動します(Telegramのリモートボット)

4か月前、私は、ほとんどのボットのような外部サーバーではなく、携帯電話で実行されるTelegramボットを作成することを考えていました。



このアイデアはゼロから生まれたものではありません。電話がジャケットやポケットに入っているときに着信やSMSを見逃すことが多かったため、追加の通知方法が必要でした。 そして、私は自分のコンピューターでTelegramを積極的に使用しているので、SMSの着信や不在着信がTelegramに届いても悪くないと思いました。 少し掘り下げた後、ボットを書くことにしました。



プロトタイプ開発



公式のドキュメントと例を使用して、Telegramボットを作成するトピックの研究を始めました。 基本的に、すべての例はPythonで書かれています。 したがって、ためらうことなく、Android上でPythonサーバーを実行する方法を探し始めました。 しかし、Pythonを学ぶ時間を評価し、サーバーの起動に適したものを見つけられなかった後、彼は代替手段を探し始め、Telegramボットを作成するためのいくつかのJavaライブラリに出会いました。 その結果、彼はPengradのプロジェクトjava- telegram -bot-apiで停止しました。



このライブラリは、その時点で、ボットを初期化し、私が必要とするメッセージを送受信することを可能にしました。 ライブラリをプロジェクトに追加したら、Telegramからメッセージを受信して​​処理するために、バックグラウンドスレッドでサイクルを開始する簡単なサービスを実装しました。 以前は、親ボット@Botfatherを介して新しいボットを登録し、そのトークンを取得する必要がありました。 ボットの作成の詳細については、 こちらをご覧ください



デバイスが画面から外れているときにシステムによってサービスが強制終了されないようにするため、サービスの開始時にWakeLockがインストールされました。



最新のメッセージを受信して​​処理のために送信できるようにする関数の例を示します。



private void getUpdates(最終的なTelegramBotボット)
private void getUpdates(final TelegramBot bot) { try { GetUpdatesResponse response = bot.execute( new GetUpdates() .limit(LIMIT) .offset(updateId.get()) .timeout(LONG_POLLING_TIMEOUT)); if (response != null && response.updates() != null && response.updates().size() > 0) { for (Update update : response.updates()) { obtainUpdate(bot, update); updateId.set(update.updateId() + 1); } } } catch (Exception e) { ErrorUtils.log(TAG, e); } }
      
      







その後、セキュリティ上の理由から、許可されたTelegramアカウントにボットをバインドする機能と、特定のユーザーに対して特定のコマンドの実行を禁止する機能を追加しました。



送信、SMSの読み取り、不在着信の表示、バッテリー情報、場所の特定など、ボットにいくつかのコマンドを追加した後、Google Playでアプリケーションを公開し、いくつかのフォーラムでトピックを作成し、コメントとフィードバックを待ち始めました。



ほとんどのレビューは良かったが、バッテリー消費量が多いという問題が明らかになった。これは、WakeLockとサービスの継続的なアクティビティによるものだと思う。少しグーグルで、AlarmManagerを介して定期的にサービスを開始し、メッセージを受信して​​応答した後にサービスを停止することにした。



これは少し役立ちましたが、別の問題が発生し、AlarmManagerが一部の中国のデバイスで正しく機能しませんでした。 そのため、ボットは、睡眠状態で数時間過ごしても目覚めないことがありました。 公式ドキュメントを調べて、メッセージを受信する方法はロングポーリングだけではなく、Webhookを使用してメッセージを受信できることを読みました。



Webhookを介したメッセージの受信



Digital Oceanにサインアップし、UbuntuでVPSを作成し、 Spark Frameworkを使用して最も単純なJava httpサーバーを実装しました。 サーバーに対して、プッシュ(webhookを介してプッシュ通知を送信)とpingの2種類の要求を行うことができます。



プッシュ通知はGoogle Firebaseを使用して送信されました。



プッシュ通知を支援するサンプルクラス
 public class PushHelper { private static final String URL = "https://fcm.googleapis.com/fcm/send"; private static java.util.logging.Logger log = java.util.logging.Logger.getLogger(PushHelper.class.getName()); private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); private static final String AUTHORIZATION = "..."; public static String push(PushRequest pushRequest) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); return post(URL, objectMapper.writeValueAsString(pushRequest)); } private static String post(String url, String json) throws IOException { RequestBody body = RequestBody.create(JSON, json); Request request = new Request.Builder() .url(url) .header("Authorization", AUTHORIZATION) .post(body) .build(); OkHttpClient client = getSslClient(); if (client != null) { Response response = client.newCall(request).execute(); return response.body().string(); } else { throw new IOException("Unable to init okhttp client"); } } ... }
      
      







プッシュ通知を送信するためのリクエストモデル
 public class PushRequest { private PushData data; //,    private String to; //-  private String priority = "high"; //  ... }
      
      





デバイスがスリープ状態にある場合でもメッセージが届くようにするには、priority = "high"を指定する必要があります



SSL証明書の生成



プッシュ通知の送信をテストした後、HTTPSを使用してサーバーを構成および起動する方法を見つけ始めました。これは、webhookを介してTelegramからメッセージを受信する際の要件の1つです。



無料の証明書はletsencrypt.orgサービスを使用して生成できますが、制限の1つは、証明書の生成時に指定されたホストをIPアドレスにできないことです。 特に、Telegram Bot APIの公式ドキュメントでは自己署名証明書の使用が許可されているため、ドメイン名をまだ登録したくなかったため、証明書の作成方法を見つけ始めました。



試行と検索に数時間費やした後、必要な証明書を生成できるスクリプトを入手しました。



create_cert.sh
 openssl req -newkey rsa:2048 -sha256 -nodes -keyout private.key -x509 -days 365 -out public_cert.pem -subj "/C=RU/ST=State/L=Location/O=Organization/CN=ServerHost" openssl pkcs12 -export -in public_cert.pem -inkey private.key -certfile public_cert.pem -out keystore.p12 keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -sigalg SHA1withRSA -destkeystore keystore.jks -deststoretype JKS rm keystore.p12 rm private.key
      
      







スクリプトを実行すると、出力は2つのファイルになります。keystore.jks-サーバーで使用され、public_cert.pem-Androidアプリケーションにwebhookをインストールするときに使用されます。



SparkフレームワークでHTTPSを開始するには、ポート(webhookに許可されるポート:443、80、88、8443)を示す1行と、生成された証明書とパスワードを示す2行を追加するだけです:



 port(8443); secure("keystore.jks", "password", null, null);
      
      





ボットのwebhookをセットアップするには、次の行をAndroidアプリケーションに追加する必要があります。



 SetWebhook setWebHook = new SetWebhook().url(WEBHOOK_URL + "/" + pushToken + "/" + secret).certificate(getCert(context)); BaseResponse res = bot.execute(setWebHook);
      
      





Webhookを登録すると、WebhookアドレスがURLとして指定され、プッシュ通知の送信に必要なプッシュトークンと、着信通知を追加で確認するために追加したデバイスで生成された秘密キーが送信されます。



RAWリソースから公開証明書を読み取る機能:



 private static byte[] getCert(Context context) throws IOException { return IOUtils.toByteArray(context.getResources().openRawResource(R.raw.public_cert)); }
      
      





Androidアプリケーションのメッセージ処理サービスを変更した後、ボットはバッテリーの消費を減らしましたが、アプリケーションが安定して動作するために必要なプッシュ通知サーバーへの依存が追加されました。



ボットを自動的に作成する



メッセージを受信するメカニズムを更新した後、BotFatherを使用してボットを作成する複雑さのために、アプリケーションが特定の割合のユーザーを使用できないという別の問題が残りました。 そのため、このプロセスを自動化することにしました。



Telegramの作成者からのtdlibライブラリはこれを助けてくれました。 残念ながら、このライブラリの使用例はほとんど見つかりませんでしたが、APIを理解したため、すべてがそれほど複雑ではないことがわかりました。 その結果、電話番号による許可をTelegramに実装し、@ Botfatherを連絡先リストに追加し、指定された連絡先(特定の場合は@Botfatherボット)とメッセージを送受信できました。



メッセージを送受信するための関数の例
 private Observable<TdApi.Message> sendMessage(long chatId, String text) { return Observable.create(subscriber -> { telegramClient.sendMessage(chatId, text, object -> { if (object instanceof TdApi.Error) { subscriber.onError(new Throwable(((TdApi.Error) object).message)); } else { TdApi.Message message = (TdApi.Message) object; subscriber.onNext(message); } }); }).delay(5, TimeUnit.SECONDS).flatMap(msg -> getLastIncomingMessage(((TdApi.Message) msg).chatId, ((TdApi.Message) msg).senderUserId, ((TdApi.Message) msg).id)); } private Observable<TdApi.Message> getLastIncomingMessage(long chatId, int userId, int outgoingMessageId) { return Observable.create(subscriber -> { telegramClient.getLastIncomingMessage(chatId, outgoingMessageId, userId, object -> { if (object instanceof TdApi.Error) { subscriber.onError(new Throwable(((TdApi.Error) object).message)); } else { TdApi.Message message = (TdApi.Message) object; subscriber.onNext(message); } }); }); }
      
      







TelegramClient.java-TdApiのラッパークラス
 public class TelegramClient { private final Client client; public TelegramClient(Context context, Client.ResultHandler updatesHandler) { TG.setDir(context.getCacheDir().getAbsolutePath()); TG.setFilesDir(context.getFilesDir().getAbsolutePath()); client = TG.getClientInstance(); TG.setUpdatesHandler(updatesHandler); } public void clearAuth(Client.ResultHandler resultHandler) { TdApi.ResetAuth request = new TdApi.ResetAuth(true); client.send(request, resultHandler); } public void getAuthState(Client.ResultHandler resultHandler) { TdApi.GetAuthState req = new TdApi.GetAuthState(); client.send(req, resultHandler); } public void sendPhone(String phone, Client.ResultHandler resultHandler) { TdApi.SetAuthPhoneNumber smsSender = new TdApi.SetAuthPhoneNumber(phone, false, true); client.send(smsSender, resultHandler); } public void checkCode(String code, String firstName, String lastName, Client.ResultHandler resultHandler) { TdApi.CheckAuthCode request = new TdApi.CheckAuthCode(code, firstName, lastName); client.send(request, resultHandler); } public void sendMessage(long chatId, String text, Client.ResultHandler resultHandler) { TdApi.InputMessageContent msg = new TdApi.InputMessageText(text, false, false, null, null); TdApi.SendMessage request = new TdApi.SendMessage(chatId, 0, false, false, null, msg); client.send(request, resultHandler); } public void getLastIncomingMessage(long chatId, int fromMessageId, int userId, Client.ResultHandler resultHandler) { getChat(chatId, chatObj -> { if (chatObj instanceof TdApi.Chat) { TdApi.GetChatHistory getChatHistory = new TdApi.GetChatHistory(chatId, fromMessageId, -1, 2); client.send(getChatHistory, messagesObj -> { if (messagesObj instanceof TdApi.Messages) { TdApi.Messages messages = (TdApi.Messages) messagesObj; if (messages.totalCount > 0) { for (TdApi.Message message : messages.messages) { if (message.id != fromMessageId && message.senderUserId != userId) { resultHandler.onResult(message); return; } } } resultHandler.onResult(new TdApi.Error(0, "Unable to get incoming message")); } else resultHandler.onResult(messagesObj); }); } else resultHandler.onResult(chatObj); }); } public void getChat(long chatId, Client.ResultHandler resultHandler) { TdApi.GetChat getChat = new TdApi.GetChat(chatId); client.send(getChat, resultHandler); } public void searchContact(String username, Client.ResultHandler resultHandler) { TdApi.SearchPublicChat searchContacts = new TdApi.SearchPublicChat(username); client.send(searchContacts, resultHandler); } public void getMe(Client.ResultHandler resultHandler) { client.send(new TdApi.GetMe(), resultHandler); } public void changeUsername(String username, Client.ResultHandler resultHandler) { client.send(new TdApi.ChangeUsername(username), resultHandler); } public void startChatWithBot(int botUserId, long chatId, Client.ResultHandler resultHandler) { TdApi.CloseChat closeChat = new TdApi.CloseChat(chatId); client.send(closeChat, resClose -> { TdApi.OpenChat openChat = new TdApi.OpenChat(chatId); client.send(openChat, resOpen -> { if (resOpen instanceof TdApi.Error) { resultHandler.onResult(resOpen); return; } TdApi.SendBotStartMessage request = new TdApi.SendBotStartMessage(botUserId, chatId, "/start"); client.send(request, resultHandler); }); }); } public void logout(Client.ResultHandler resultHandler) { client.send(new TdApi.ResetAuth(false), resultHandler); } }
      
      







新機能の追加



自律性に関する主要な問題を解決した後、新しいチームを追加し始めました。

その結果、次のようなコマンドが追加されました:写真、ビデオ録画、ボイスレコーダー、スクリーンショット、プレーヤーコントロール、選択したアプリケーションの起動など。 コマンドを簡単に起動できるように、Telegramキーボードを追加し、コマンドをカテゴリに分けました。



ユーザーの要求に応じて、Taskerコマンドを呼び出し、TaskerからTelegramにメッセージを送信する機能も追加しました。



その後、Telegramにメッセージを送信するために、サードパーティアプリケーションからの外部アクセスを追加するといいと思いました。 メッセージはテキストであり、音声、ビデオ、座標による位置を含むことができます。 その結果、プロジェクトに追加できるライブラリを作成しました。



図書館

使用例



おわりに



この記事では、Androidデバイスで実行されるボットを作成するプロジェクトに取り組んだ簡単な歴史と、私が遭遇した困難を共有しようとしました。 現在、私は自由時間にプロジェクトに従事しており、新しいチームを追加して、発生したエラーを修正しています。



ご清聴ありがとうございました。 有益なコメントや提案をお聞かせください。



参照:

Google Playのアプリケーション

電報のチャンネル

プロジェクトサイト



All Articles