robot.brain を使った Hubot スクリプトをつくろう

これは [Hubot Advent Calendar 2014][hubot-adventar-2014] の 18 日目の記事です。

また、今回は @bouzuya の Hubot 連載の第 11 回です。[目次は、第 1 回の記事にある][hubot-adventar-2014-bouzuya-index]ので、そちらをどうぞ。

ちなみに昨日は @hiconyan さんの [Hubotで「どのコマンドにも一致しない」ときの処理][hubot-adventar-2014-17] でした。`Robot#helpCommands()` については説明する予定だったんだけど、もういいかなー。

# 前回まで、そして今回は

さて、前回は [設定を読み込む Hubot スクリプトをつくろう][hubot-adventar-2014-16] ということで、外部 (環境変数) から設定を読み込む Hubot スクリプトをつくりました。

今回は、`robot.brain` を使って Hubot スクリプトのデータを永続化してみましょう。

`brain` はシンプルな KVS であり、Hubot の標準の永続化の仕組みです。実際にどうやって永続化するのかは次回説明します。今回は使うだけです。

# サンプルコード

以下のサンプルコードは、ちょっと長いのですが、メッセージを 7 日後に予約できる Hubot スクリプトです。

Heroku にデプロイした場合、単純にメモリーに保持すると 7 日後までに何度も再起動がかかり、予約情報を失ってしまいます。そこで、予約情報を brain に書き込むことで永続化します(どのような形式で永続化するかはまた後日紹介します)。永続化しておけば、再起動がかかってもデータが失われる心配はありません。

```coffeescript:7days-later.coffee
# Description
#   A Hubot script for sending a message to yourself seven days later.
#
# Configuration:
#   None
#
# Commands:
#   hubot 7d add <message> - reserve the sending message
#   hubot 7d list - list reserved messages
#
# Author:
#   bouzuya <m@bouzuya.net>

moment = require 'moment'

module.exports = (robot) ->
  key = '7days-later'

  robot.respond /7d add (\S+)$/, (msg) ->
    message = msg.match[1]
    messages = robot.brain.get(key) ? []
    i = { room: msg.envelope.room, at: moment().add(7, 'days'), message }
    messages.push i
    robot.brain.set(key, messages)
    msg.send "#{i.at.format('YYYY-MM-DD HH:mm')} #{i.message}"

  robot.respond /7d list$/, (msg) ->
    messages = robot.brain.get(key) ? []
    message = messages.sort (a, b) ->
      if a.at.isSame(b.at)
        0
      else if a.at.isBefore(b.at)
        -1
      else
        1
    .map (i) ->
      "#{i.at.format('YYYY-MM-DD HH:mm')} #{i.message}"
    .join '\n'
    msg.send message

  watch = ->
    setTimeout ->
      messages = robot.brain.get(key) ? []
      now = moment()
      targets = messages.filter (i) -> i.at.isBefore(now)
      newMessages = messages.filter (i) -> !i.at.isBefore(now)
      if messages.length isnt newMessages.length
        robot.brain.set(key, newMessages)
      targets.forEach (i) ->
        robot.messageRoom i.room, i.message
      watch()
    , 60000

  watch()
```

# 解説

大きく分けて、3 つになります。

- 予約を追加する `robot.respond /7d add (\S+)$/, (msg) ->`
- 予約を一覧表示する `robot.respond /7d list$/, (msg) ->`
- メッセージを送信する `watch = ->`

## 予約の追加

```coffeescript
  robot.respond /7d add (\S+)$/, (msg) ->
    message = msg.match[1]
    messages = robot.brain.get(key) ? []
    i = { room: msg.envelope.room, at: moment().add(7, 'days'), message }
    messages.push i
    robot.brain.set(key, messages)
    msg.send "#{i.at.format('YYYY-MM-DD HH:mm')} #{i.message}"
```

ポイントは brain です。

### brain からの値取得

`messages = robot.brain.get(key) ? []`

brain から値を取得しています。もし取得できなかった場合には、nullが返されるので、代わりに空の配列を設定しています。

### brain への値設定

`robot.brain.set(key, messages)`

brain へ値を設定しています。

## 予約の一覧

特に言うことはないですね。 `robot.brain.get(key)` で取得して表示するだけですね。

## メッセージの送信

```coffeescript
  watch = ->
    setTimeout ->
      messages = robot.brain.get(key) ? []
      now = moment()
      targets = messages.filter (i) -> i.at.isBefore(now)
      newMessages = messages.filter (i) -> !i.at.isBefore(now)
      if messages.length isnt newMessages.length
        robot.brain.set(key, newMessages)
      targets.forEach (i) ->
        robot.messageRoom i.room, i.message
      watch()
    , 60000

  watch()
```

`setTimeout()` を使って定期的に メッセージを確認しています。

`setInterval()` を使って定期的に確認する方法もありますが、一回ずつ実行したいので、今回は `setTimeout()` を使っています。

# 動かしてみましょう

念のため動かしましょう。

```
$ HUBOT_SHELL_USER_NAME='bouzuya' PATH="./node_modules/hubot/node_modules/.bin:$PATH" $(npm bin)/hubot -a shell -n hubot -r src                               
hubot> hubot 7d add hoge
2014-12-25 23:33 hoge
hubot> hubot 7d add 忘れたころにやってくる
2014-12-25 23:34 忘れたころにやってくる
hubot> hubot 7d list
2014-12-25 23:33 hoge
2014-12-25 23:34 忘れたころにやってくる
```

7 日後なので、確認できないのですが、たぶん来ます。

# まとめ

`robot.brain` の値の取得・設定を説明しました。

実際にどうやって永続化するのかや、問題点などは次回説明します。

今回の Hubot スクリプトは [bouzuya/hubot-7days-later][] に置いています。動かない場合には参考にしてみてください。

# 最後に

実は今回は [bouzuya/hubot-brainfxxk][] というボツになったネタがあって、brain といえば brainf*ck だろうと思い、 をつくったのだけど、余計なコードが多すぎるので却下されました。

というか、brain 嫌いなんですよね。

[getting-started-with-hubot]: https://github.com/github/hubot/tree/v2.9.3/docs#getting-started-with-hubot
[bouzuya/hubot-hello-world]: https://github.com/bouzuya/hubot-hello-world/
[bouzuya/hubot-choice]: https://github.com/bouzuya/hubot-choice/
[bouzuya/hubot-adventar]: https://github.com/bouzuya/hubot-adventar/
[bouzuya/hubot-homepage]: https://github.com/bouzuya/hubot-homepage/
[bouzuya/hubot-script-gist]: https://github.com/bouzuya/hubot-script-gist/
[bouzuya/hubot-brainfxxk]: https://github.com/bouzuya/hubot-brainfxxk/
[bouzuya/hubot-7days-later]: https://github.com/bouzuya/hubot-7days-later/
[hubot-script-per-day-61]: http://bouzuya.hatenablog.com/entry/2014/09/12/224215
[github/hubot]: https://github.com/github/hubot/
[github/generator-hubot]: https://github.com/github/generator-hubot/
[ops-for-everyone]: https://speakerdeck.com/johndbritton/ops-for-everyone
[adventar]: http://www.adventar.org/
[express-reference]: http://expressjs.com/3x/api.html
[hubot.github.com]: https://hubot.github.com/
[hubot-adventar-2014]: http://www.adventar.org/calendars/384
[hubot-adventar-2014-bouzuya-index]: http://qiita.com/bouzuya/items/c7d0ad80c357aab6b696
[hubot-adventar-2014-1]: http://qiita.com/bouzuya/items/c7d0ad80c357aab6b696
[hubot-adventar-2014-2]: http://qiita.com/bouzuya/items/11c0c6da2b3ad54b827f
[hubot-adventar-2014-3]: http://qiita.com/bouzuya/items/2a200c9e8a45e2478bc2
[hubot-adventar-2014-4]: http://qiita.com/bouzuya/items/4c0206d72ff22ade9339
[hubot-adventar-2014-5]: http://pasero.net/~mako/blog/s/685
[hubot-adventar-2014-6]: http://qiita.com/bouzuya/items/4e051f68f8d68a2944b0
[hubot-adventar-2014-7]: http://qiita.com/bouzuya/items/2e935c99647b93d8ee1d
[hubot-adventar-2014-8]: http://udzura.hatenablog.jp/entry/2014/12/08/182744
[hubot-adventar-2014-9]: http://qiita.com/bouzuya/items/587c4e920151342e6f1d
[hubot-adventar-2014-10]: http://qiita.com/bouzuya/items/8e8ee8e2e8f83513ad35
[hubot-adventar-2014-11]: http://qiita.com/bouzuya/items/3e239bc530e7c2c3a26c
[hubot-adventar-2014-12]: http://qiita.com/hiconyan/items/3e5481a61657890624dc
[hubot-adventar-2014-13]: http://pasero.net/~mako/blog/s/689
[hubot-adventar-2014-16]: http://qiita.com/bouzuya/items/d65a394cac9e76b56d3d
[hubot-adventar-2014-17]: http://qiita.com/hiconyan/items/baf6ac56129a26d8ac0c