(備忘録) use Telegram Bot
Live With Linux::script
- TOP
- Articles
- Live With Linux
- (備忘録) use Telegram Bot
序
本記事は、以前テストで作った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リクエストによって行うため、結構時間がかかってしまうのだ。
POST
やPUT
に対して非同期に応答したいのは日常的な要求なので、トリック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
コマンドを使う。
id
はchat_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"}]
]
}
}
インラインキーボードを連続で使う場合、新規メッセージを投げるのではなく、既存メッセージを更新するほうが良いみたい。