Rubotyがサーバーレスで動くようにした

この記事は [PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos) の25日目の記事です。
昨日は[@kerimeka](https://qiita.com/kerimeka)さんの[ゲームにおけるFirebase 活用例](https://qiita.com/kerimeka/items/9cee8a122d815b3186d2)でした。

🎉🎁何とか全枠埋まりました！🎁🎉
記事を書いて頂いたみなさん、ありがとうございました！

------

[Ruboty](https://github.com/r7kamura/ruboty)という、Rubyでチャットボットが作れるOSSがあります。
個人的に凄く好きなOSSで、何とか趣味なり仕事で使えないかと考えています。(趣味方面だと、[@gymtter](https://twitter.com/gymtter)で一応使ってる。)


## やりたかったこと

Rubotyはサーバー上に常駐して発言を待ち受け続けますが、これだと常に一定のリソースを占有します。(HerokuだったりEC2だったり。)
利用頻度の高いBotならまだしも、1時間に数回みたいな使い方だと非常に勿体ないので、サーバーレス化したいなと思いました。

会社ではチャットツールとしてChatworkを使っているんですが、Chatwork APIでは特定ルームに投稿があったときにWebhookを投げることができます。

参考: [Chatwork APIドキュメント](http://developer.chatwork.com/ja/webhook.html)

このWebhookをトリガーにしてRubotyが動くようにすれば良さそうです。

## やったこと

### 概要

こんな感じに動くようにしました。

1. Chatworkで発言
2. Chatwork APIのWebhook設定が発火
3. API Gatewayのエンドポイントを叩く
4. Lambdaに載っているRubotyを実行
5. Chatworkに返事を投稿 

Lambdaに載せているのは↓のリポジトリです。

[honeniq/headless-ruboty](https://github.com/honeniq/headless-ruboty)

検証では[Serverless Framework](https://serverless.com/)を使ってAWSの環境構築を省力化しているんですが、``serverless.yml``の整理まで追いつかなかったので、まだリポジトリには載せていません。


### Rubotyの挙動

Ruboty本来の動きはこんな感じです。

1. 必要な諸々を読み込む
2. 入力を待ち受ける
3. Rubotyに対するコマンド(として認識できる文字列)を受け取る
4. ハンドラーの中から該当するコマンドを探す
5. 対応するアクションを実行する
6. 2の待ち受け状態に戻る

今回の用途では、3から5だけ使えれば良さそうです。


### pryで何が必要か調べる

pryでRubotyのメソッドを色々叩いてみて、必要なやつを探しました。

``` ruby:
[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``が呼ばれます。

``` ruby:handler.rb
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でそのまま入っています。

再掲: [Chatwork APIドキュメント](http://developer.chatwork.com/ja/webhook.html)

``` json:sample_webhook
{
    "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

``` ruby: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](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/robot.rb#L16-L25)でやっている処理から、待ち受け処理やデーモン化に必要な部分を除き、``receive``を呼んでいるだけです。



## 結果

Chatworkのマイチャット上でRubotyに話しかけると、同じチャットに返事が返ってくるようになりました。🎉
(今回はRubotyも自分のアカウントで投稿しているので、ただの独り言みたいに見えるけど。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2492/c24e08b7-013b-2528-c0e9-0509befff89a.png)