この記事は PONOS Advent Calendar 2019 の25日目の記事です。
昨日は@kerimekaさんのゲームにおけるFirebase 活用例でした。
🎉🎁何とか全枠埋まりました!🎁🎉
記事を書いて頂いたみなさん、ありがとうございました!
Rubotyという、Rubyでチャットボットが作れるOSSがあります。
個人的に凄く好きなOSSで、何とか趣味なり仕事で使えないかと考えています。(趣味方面だと、@gymtterで一応使ってる。)
やりたかったこと
Rubotyはサーバー上に常駐して発言を待ち受け続けますが、これだと常に一定のリソースを占有します。(HerokuだったりEC2だったり。)
利用頻度の高いBotならまだしも、1時間に数回みたいな使い方だと非常に勿体ないので、サーバーレス化したいなと思いました。
会社ではチャットツールとしてChatworkを使っているんですが、Chatwork APIでは特定ルームに投稿があったときにWebhookを投げることができます。
このWebhookをトリガーにしてRubotyが動くようにすれば良さそうです。
やったこと
概要
こんな感じに動くようにしました。
- Chatworkで発言
- Chatwork APIのWebhook設定が発火
- API Gatewayのエンドポイントを叩く
- Lambdaに載っているRubotyを実行
- Chatworkに返事を投稿
Lambdaに載せているのは↓のリポジトリです。
検証ではServerless Frameworkを使ってAWSの環境構築を省力化しているんですが、serverless.yml
の整理まで追いつかなかったので、まだリポジトリには載せていません。
Rubotyの挙動
Ruboty本来の動きはこんな感じです。
- 必要な諸々を読み込む
- 入力を待ち受ける
- Rubotyに対するコマンド(として認識できる文字列)を受け取る
- ハンドラーの中から該当するコマンドを探す
- 対応するアクションを実行する
- 2の待ち受け状態に戻る
今回の用途では、3から5だけ使えれば良さそうです。
pryで何が必要か調べる
pryでRubotyのメソッドを色々叩いてみて、必要なやつを探しました。
[1] pry(main)> require 'ruboty'
=> true
[2] pry(main)> bot = Ruboty::Robot.new
=> #<Ruboty::Robot:0x00007fc2b31c47d8 @options={}>
[3] pry(main)> bot.receive(body:'ruboty help')
ruboty /help( me)?(?: (?<filter>.+))?\z/i - Show this help message
ruboty /ping\z/i - Return PONG to PING
ruboty /who am i\?/i - Answer who you are
=> nil
初期状態のRubotyは標準出力に返事を返すので、この動作で合っていそうです。🎉
実装: handler.rb
Lambdaのエントリーポイントです。
ChatworkからAPI GatewayのエンドポイントにWebhookが飛んでくると、下記のtalk
が呼ばれます。
def talk(event:, context:)
message = JSON.parse(event['body'])['webhook_event']['body']
@robot = Ruboty::Robot.new
@robot.headless_run(message)
{
statusCode: 200,
body: {
message: 'done',
}.to_json
}
end
Lambdaから渡されるevent
はHashで、event['body']
にはChatworkが投げてきたWebhookのJSONがStringでそのまま入っています。
{
"webhook_setting_id": "12345",
"webhook_event_type": "mention_to_me",
"webhook_event_time": 1498028130,
"webhook_event":{
"from_account_id": 123456,
"to_account_id": 1484814,
"room_id": 567890123,
"message_id": "789012345",
"body": "[To:1484814]おかずはなんですか?",
"send_time": 1498028125,
"update_time": 0
}
}
最終的に取り出したいのはwebhook_event
の中のbody
なので、JSONをパースして対象の要素まで掘っています。
最後にステータスコード200を返しているのは、Chatwork APIの仕様に対応するためです。
実装: lib/ruboty/headless_run.rb
require 'ruboty'
module Ruboty
class Robot
def headless_run(message)
dotenv
bundle
setup
remember
handle
adapter
receive(body: message)
end
end
end
Ruboty::Robot
に新しいメソッドheadless_run
を生やします。
このメソッドは、本家のRuboty::Robot#runでやっている処理から、待ち受け処理やデーモン化に必要な部分を除き、receive
を呼んでいるだけです。
結果
Chatworkのマイチャット上でRubotyに話しかけると、同じチャットに返事が返ってくるようになりました。🎉
(今回はRubotyも自分のアカウントで投稿しているので、ただの独り言みたいに見えるけど。)