🔙 Back to Top

Thu Nov 16 2017

Twitter bot イライザの開発

かつて

イライザ (@ampeloss) という人口無能 bot を開発していた. イライザという名前が指すように、当初は、超単純な正規表現パターンでマッチさせて会話をするようなものを作って遊ぶおもちゃだったが、 いつからか対話はどうでもよくなって、単に、「天気を教えて」とリプライをすると今現在の天気をどこからか取得して〜といった便利使いするためのものになった.

ところでイライザというのは本当にただ1個のスクリプトで元々はできていて、 「天気を教えて」 にマッチできるようなパターンマッチから、実際に天気を取得するコードまでを中にハードコーディングしていた. ようやくライブラリ化を覚えて天気を取得するコードは別個にするようにしていったが、 「天気を教えて」パターンはやはり、メインループのなかにベタ書きしていた. 要するに、機能をあれもこれも増やす中でコードがめちゃくちゃになっていった.

ライブラリをもっと形式的なものにする必要があった. 例えば modules/forecast/ というディレクトリを切って、その中に天気を取得する main.js (node で書いているので) と、 発動するパターンを列挙した patterns.json を置いておく. こういうふうにすればマシではある.

しかし機能ごとにパターンを考えるのは面倒だ. パターンを舐める順番によってこれは発動するがアレが発動しない、といったスパゲッティ状態もしばしば起こっていた.

https://github.com/cympfh/eliza 昨日の数だけディレクトリや hogehoge.js が並んでいて目も当てられない.

いま

まず、イライザは一つのプロジェクトではなく、機能ごとに別々のレポジトリで管理するような集合体にした. 例えば一つは見ているアニメの時間の五分前になったらリプライで知らせる、という機能がある. これはシェルスクリプトで実装してある. anime-notify.sh という名前なのだが、この機能を実行するためにはこのスクリプトを起動させておく.

ということは機能の数だけプロセスを実行する必要がある. 機能が N 個ある場合、イライザを完全な状態で立ち上げるためには N 回起動する必要がある.

今のところ、N は 3 なので、実際、手でそうしてある. とはいえいつか辛くなることは目に見えているのでその場合は、そのようなスクリプトを書くだけだと思っている.

twitter-shellbot

N が 3 だと言ったが、その中の一つがメインで、これで多くの機能をカバーするつもりなので、本当に機能が3つしかないわけではない. これには twitter-shellbot という名前をつけている. あんまりネーミングセンスはない. これはTwitter上の対話で、ある機能を呼び出し、その結果をリプライ等の形で受け取る. まさに天気予報を取得してもらう機能などはこれで実現している. しかも、基本的には、設定を少し書き換えるだけで、新しく機能を追加できる. つまり新しい機能を twitter-shellbot の中に追加実装する、という形はとっていない. どうするかと言えば、機能を全て、CUIコマンドとして実装する形を取っている. 例えば現在の天気、天気予報を取得するコマンドは、この中には入っておらず、全然別なところにある. CUIのコマンドなので、イライザを介す必要も実はない.

tenki というコマンド名で、叩くと、現在の東京の天気を取得する. 現在ではなく、一週間の予報とか、場所の変更などもできる. この中身はというと、 openweathermap が提供しているAPIを叩いているだけである (curl して jq するだけ).

$ tenki
Tokyo,JP
10.9°C (10°C/13°C)
1010hPa
Clouds (few clouds)

Twitter 上でイライザにフォローされている状態で ~tenki というツイートを見かけたとする. ここで ~ は今適当に指定しているプレフィックスで、設定から自由に変更できる. とにかく、イライザへのコマンドであることがわかればいい. イライザはそのようなツイートを見かけたら、 プレフィックスを除去し、 また # より後ろの文字列をコメントアウトし、そしてまんまプロセスの実行をする. シェルでのプロセスの実行と同じなので ~tenki -f Kyoto みたいにコマンドライン引数を与えて実行もできる. プロセスの出力をリプライで返す.

シェルとして〜とは言ったが、シェルスクリプト特有の文法、たとえばパイプやリダイレクトは対応していない. もしそういうのに対応したいなら、内部で bash を呼んでしまえばいいが (よくある) 脆弱性が怖いのでしてない.

脆弱性で言うと、なんでもかんでもコマンドを許すのも嫌なので、ホワイトリスト方式で実行を許すコマンドのリストを設定に書いてある. PATH が通ってて、かつ、ホワイトリストの中に入ってあるコマンド全てが実行可能である. あと加えて、その場の ./bin の下にあるスクリプトは無条件で実行可能にする (PATH への追加もしてある).

このイライザの設計の良さは、機能をイライザのためだけに実装するのではなく、 普段から自分で使えるCUIコマンドを実装すれば副次的にイライザにも追加できることにある. イライザのためだけに特別に作成した機能は ./bin に入れておけばいい.

その他

このままだとテキストのリプライしかできない. 出力をイライザ用の形式にすることにする.

出力が RT から始まる場合、それはテキストのリプライではなく、RTをするというアクションの指示であると見做す. まだ必要に迫られてないので考えてないが画像をツイートするようなフォーマットもあると良さそう.

どのフォーマットにも当てはまらない場合は、ただそのテキストをツイートする. あくまでもただのコマンドとして利用できることを目指している.

まとめ

機能を追加する場合には、 CUIコマンドとして実装して、PATH の通ったところに置いておいて、設定にコマンド名を一つ追加するだけでよい. 拡張性に優れた設計であると今のところ信じている.