Chienomi

(備忘録) use Telegram Bot

Live With Linux::script

本記事は、以前テストで作ったTelegram Botを完全にふっとばしてしまったので、新しくTelegram Botを作るのに苦労したことから、記録として残しておくものである。

Telegramは非常に優れたUIを持っており、特にBotに関しては強烈な優秀さなので、Telegram Botを作ると色々捗る。

基本的な概要

  • Bot自体の作成はBotFatherというTelegramBotを介して行う
  • BotFatherでbotを作るとtokenが手に入る。Telegram botのAPIはすべてhttps://api.telegram.org/bot${TOKEN}/${COMMAND}になってる
  • Botの発言や操作、情報取得などはこのAPIにHTTPリクエストを投げることで実現可能
  • Botに対する問いかけに対応するには、setWebhookコマンドで設定をする。POSTリクエストで、最低限{"url": url}を投げればOK

通知botとしてはDiscordのほうが簡単に作れるけど、一応大差ないレベルで作成自体はできる。 botとして運用することを考えるとDiscordよりももっと楽。

ただし、作成したbotはpublicになるから、プログラム側で工夫が必要。

メッセージを受け取るためにHTTPSのリクエストを受け取れる必要がある。 トラフィックが少なければレンタルサーバーでCGIで作っても問題はない。

Botの準備

BotFatherで先にBotを作ってしまってもいいけど、アプリを先に用意するほうが良さそう。

HTTPSで受けてHTTPSで投げる形なので、どういう形態のbotであるかによって話が変わってくる。 Discord Webhookのように投稿される「だけ」のものを想定するならば、サーバーからだろうがPCからだろうがcurlとかで通知を投げるようにすればいいだけで、BotFatherでtokenだけ作れば準備完了だ。

一方、受信時はHTTPSで受けられればOKだから、個人的に使うものくらいであればCGIで事足りる。 実際、今回私はレンタルサーバーのConoHa WINGでCGIを用意した。

改めて確認するが、メッセージの送信はTelegramに対するHTTPSリクエストで行い、メッセージの受信はTelegramからのHTTPSリクエストの受信で行う。 HTTPSで受けたリクエストに対して意味のある中身を返す必要は基本的になく、常時204を返却すれば良い。 非正常の応答を返した場合、Telegramはリトライしてくる。このため、リクエストが溢れてしまうことがあるので要注意。Telegram側の無用な負荷にもなる。

なお、今回はRack/CGIによって作ったが、Rackは(方法があるのかもしれないが)callメソッドが終了しないと応答できないため、さっさと204を返してしまうということができず、あんまり向いていないように感じた。 というのも、メッセージを受信して応答すると、応答は(同期的な)HTTPSリクエストによって行うため、結構時間がかかってしまうのだ。

POSTPUTに対して非同期に応答したいのは日常的な要求なので、トリック1を使わなくても何かしら方法はあるように思われるが、単純にcallを使って非同期に投げてしまうと、それを回収するチャンスがない。CGIならプロセスは死ぬのでThread投げっぱなしでもいいが、ちょっとこれは大きなアプリケーションを作る上で足かせになる。 Railsがイケてないのもこれが一因としてあるかもしれない。

TelegramはsetWebhookしなければ、メッセージを受信したときにそれをどこにも通知しない。発信オンリーなbotはDiscordと大差ない感覚で作ることができる。もちろん、サーバーの中でチャンネル分けができるDiscordのほうが適性はある。

受信したいのであれば、何らかの形でアプリを用意する。 この場合も先にBotFatherでtokenを獲得しても構わない(tokenがないと送信する部分が書けないので自然とそうなる)。 setWebhookを投げるのはちゃんと204を返せるようにしてから。

メッセージを受け取って打ち返す

setWebhookで設定したアドレスにJSONで投げてくる。 設定は難しくないけど、ちゃんと受け取れるようになってないとsetWebhookが失敗する(この時点でドメインを引きにいくため)。 あと、Telegram側のDNSの更新がそんなに早くないので、新規に追加したドメインはなかなか拾いに行ってくれない。

中身はこんな感じ。

update_id: *Int
message:
  message_id: *Int
  from:
    id: *Int
    is_bot: false
    first_name: Haruka
    last_name: Masaki
    username: *String
    language_code: ja
  chat:
    id: *Int
    first_name: Haruka
    last_name: Masaki
    username: *String
    type: private
  date: 1662926766
  text: hell yeah

一番重要なのがid。 最大52bitの数値で、ルームのIDになっている。 相手ユーザーのIDではないのは、botが入っているのは1対1のチャットとは限らないからだと思われる。 typeにはprivate, group, supergroup, channelのいずれかが入り、private以外はtitleも入る。

打ち返すにはsendMessageコマンドを使う。 idchat_idに入り、テキストをtextに入れれば良い。

{
    "chat_id": 1234567890,
    "text": "Hell yeah!"
}

メッセージの形式のよって中身の構造も結構変わり、テキストを確実に獲得する方法はない。 想定しないメッセージタイプがきたときに204を返せるように作ったほうがいい。

メニュー

メニューを作るとbotの機能を選択して呼び出すことができる。

  • MenuButtonCommands
  • MenuButtonWebApp
  • MenuButtonDefault

の3種類があり、簡単なのはMenuButtonCommands

setMyCommandsでコマンドを定義でき、それがリストされる。

メニューの構築はBotFatherで行うことができる。 /setcommandsを使ってコマンドを定義する。 デフォルトでこのコマンド一覧が使われる。

カスタムキーボード

コンテキストに合わせてボタンが表示されるカスタムキーボードを定義することができる。

もろもろのメッセージにあるreply_markupの値として設定することになる。 型はInlineKeyboardMarkup

単純には

"reply_markup": {"keyboard": [
    [
        {
            "text": "example"
        }
    ]
]}

って感じ。配列の配列になっているのは、行単位で設定可能なため。

インラインキーボード(メッセージ側に出るやつ)を使う場合keyboardの代わりにinline_keyboardを使えばいいんだけど、そっちはcallback_dataの設定が必要。 (他の項目を設定してもいけるけど)

keyboardを消す場合、

"reply_markup": {
    "remove_keyboard": true
}

となる。キーボード使う場合、これはかなり意識する必要がある。

実例:

{
    "chat_id": 1234567890,
    "text": "Hell yeah!",
    "reply_markup": {
        "reply_keyboard": [
            [{"text": "Hell", "callback_data": "helle"}, {"text": "Yeah", "callback_data": "yeahe"}]
        ]
    }
}

インラインキーボードを連続で使う場合、新規メッセージを投げるのではなく、既存メッセージを更新するほうが良いみたい。