Hubot:概要〜インストール〜実用的なモノが作れるまで


GitHubが作ったチャットのbotフレームワーク、Hubotを使ってみる(Mac)。
これがあればいろんなチャットツールにbotを導入できる！

ここでは、Hubotのインストールと各種機能のスクリプトの実装の説明をする。
各種チャットツールとの連携方法やHerokuへのデプロイ方法などはココでは説明しない。

## Hubotの構造
Hubotは以下の構造になっている

[チャットツール]
　↑↓
[Adapter] : チャットツールとRobotとの橋渡し。ここを入れ替えることでいろんなチャットツールに対応可能。
　↑↓
[Robot]：Hubot本体。
　↑↓
[scripts]：botの挙動を実装したスクリプト。CoffeeScriptで書く。

## Hubotのインストール
Hubotは[node.js](http://nodejs.jp/)で動くので、まずnode.jsをインストール。[(node.js参考)](http://gihyo.jp/dev/serial/01/nodejs/0001)

```
$ brew install node
```

そしてHubotのジェネレータをインストール。
`npm`はnode.jsのパッケージ管理ツールで、node.jsをインストールすると使えるようになる。
`yo`は対話形式でプロジェクトのひな形を生成する[Yeoman](http://yeoman.io/)というツールを構成するものの一つ。[(Yeoman参考)](http://www.atmarkit.co.jp/ait/articles/1407/02/news040.html)

```
$ npm install -g yo generator-hubot
```

任意のディレクトリでプロジェクトを作成

```
$ mkdir myhubot
$ cd myhubot
$ yo hubot
```

まず、yeoman改善のために匿名でフィードバックを収集していいか聞かれるのでy/nでお返事。

```
[?] ==========================================================================
We're constantly looking for ways to make yo better!
May we anonymously report usage statistics to improve the tool over time?
More info: https://github.com/yeoman/insight & http://yeoman.io
==========================================================================:
```

その後、対話形式でのインストールとなる。以下に答える。
はじめはデフォルト値がカッコ書きで入ってるけど、何か入力したらそちらに上書きされる。

* 製作者名
* botの名前
* botの説明
* Adapterに何を使うか(どのチャットツール向けに作るか)

adapterは、`irc` `twitter` `hipchat` `chatwork` `slack` などがあるみたい。

```
                     _____________________________
                    /                             \
   //\              |      Extracting input for    |
  ////\    _____    |   self-replication process   |
 //////\  /_____\   \                             /
 ======= |[^_/\_]|   /----------------------------
  |   | _|___@@__|__
  +===+/  ///     \_\
   | |_\ /// HUBOT/\\
   |___/\//      /  \\
         \      /   +---+
          \____/    |   |
           | //|    +===+
            \//      |xx|

? Owner: kta-m
? Bot name: myhubot
? Description: my first hubot
? Bot adapter: slack
   create bin/hubot
   create bin/hubot.cmd
   create Procfile
   create README.md
   create external-scripts.json
   create hubot-scripts.json
   create .gitignore
   create package.json
   create scripts/example.coffee
   create .editorconfig
                     _____________________________
 _____              /                             \
 \    \             |   Self-replication process   |
 |    |    _____    |          complete...         |
 |__\\|   /_____\   \     Good luck with that.    /
   |//+  |[^_/\_]|   /----------------------------
  |   | _|___@@__|__
  +===+/  ///     \_\
   | |_\ /// HUBOT/\\
   |___/\//      /  \\
         \      /   +---+
          \____/    |   |
           | //|    +===+
            \//      |xx|
```
あとはいい感じにいろいろインストールしてくれる。

インストールが完了したら、試しにローカルで動作確認してみる。

```
$ bin/hubot --name myhubot
```

warningがいくつか出るかもだけど、気にせずhubotに語りかけてみる。
名前を指定するのが面倒なら、`--name`は付けなくても大丈夫。ただ、その場合は`hubot`で呼びかけないと反応してくれない。

```
Hubot> hubot ping
Hubot> PONG
```

他にデフォルトでできることは`help`コマンドで見れる。

```
Hubot> hubot help
Hubot> Events:
debug - {user: <user object to send message to>}
Hubot <user> is a badass guitarist - assign a role to a user
Hubot <user> is not a badass guitarist - remove a role from a user
Hubot animate me <query> - The same thing as `image me`, except adds a few parameters to try to return an animated GIF instead.
Hubot die - End Hubot process
Hubot die - End Hubot process
Hubot echo <text> - Reply back with <text>
Hubot echo <text> - Reply back with <text>
Hubot fake event <event> - Triggers the <event> event for debugging reasons
Hubot help - Displays all of the help commands that Hubot knows about.
Hubot help <query> - Displays all help commands that match <query>.
Hubot image me <query> - The Original. Queries Google Images for <query> and returns a random top result.
Hubot map me <query> - Returns a map view of the area returned by `query`.
Hubot mustache me <query> - Searches Google Images for the specified query and mustaches it.
Hubot mustache me <url> - Adds a mustache to the specified URL.
Hubot ping - Reply with pong
Hubot ping - Reply with pong
Hubot pug bomb N - get N pugs
Hubot pug me - Receive a pug
Hubot show storage - Display the contents that are persisted in the brain
Hubot show users - Display all users that Hubot knows about
Hubot the rules - Make sure Hubot still knows the rules.
Hubot time - Reply with current time
Hubot time - Reply with current time
Hubot translate me <phrase> - Searches for a translation for the <phrase> and then prints that bad boy out.
Hubot translate me from <source> into <target> <phrase> - Translates <phrase> from <source> into <target>. Both <source> and <target> are optional
Hubot who is <user> - see what roles a user has
Hubot youtube me <query> - Searches YouTube for the query and returns the video embed link.
ship it - Display a motivation squirrel
```

## スクリプトを書く
独自スクリプトは、`scripts`ディレクトリに入れる。

### 呼びかけに応える(respond)
明示的にhubotに対して呼びかけを行なったときに反応する。
呼びかけ内容は正規表現で判別。マッチした箇所も取得できる。
呼びかけ方法はいろいろあって、どれでもOK。

```
MYHUBOT xxx
myhubot xxx
@myhubot xxx
myhubot: xxx
```

#### 実装

```hoge.coffee
module.exports = (robot) ->

  robot.respond /つー/i, (msg) ->
    msg.send "かー"

  # 正規表現でマッチングした部分の取得もできる
  robot.respond /I am (.*)/i, (msg) ->
    msg.send "Hi, #{msg.match[1]}"

  # msg.randomで配列の文字列の中からランダムに選択して発言する
  robot.respond /おみくじ/i, (msg) ->
    msg.send msg.random ["大吉", "中吉", "小吉", "凶"]
```

#### 結果

```
Hubot> myhubot つー
Hubot> かー
Hubot> myhubot I am Ken
Hubot> Hi, Ken
Hubot> myhuobt おみくじ
Hubot> 大吉
```

### チャット上の発言に反応する(hear)
チャット上の発言に特定の文字列が含まれていたら反応する。

#### 実装

```hoge.coffee
module.exports = (robot) ->

  robot.hear /疲れた/i, (msg) ->
    msg.send "がんばって！"
```

#### 結果

```
Hubot> もう疲れたよ。。。
Hubot> がんばって！
```

### 定期的に発言する(cron)
cronモジュールが必要なので、`package.json`の中に[cronモジュール](https://github.com/ncb000gt/node-cron)を追加する。
また、標準だとUTCなので、タイムゾーンを指定したい場合は別途[timeモジュール](https://github.com/TooTallNate/node-time)が必要。
`npm install`とかしなくても、初回実行時にインストールしてくれる。


```package.json
{
  (略)
  "dependencies": {
    (略)
    "cron": "^1.0.5",
    "time": "^0.11.0"
  },
  (略)
}
```

実行時間の指定はLinuxのcronと同じようにできるけど、
こちらは秒単位で指定できるので、要素が6つあるので注意。

#### 実装

```hoge.coffee
cronJob = require('cron').CronJob
 
module.exports = (robot) ->
  cronjob = new cronJob(
    cronTime: "0 0 9 * * *"     # 実行時間
    start:    true              # すぐにcronのjobを実行するか
    timeZone: "Asia/Tokyo"      # タイムゾーン指定
    onTick: ->                  # 時間が来た時に実行する処理
      robot.send {room: "#ROOM_NAME"}, "おはようございます！"
    )
```

#### 結果

(毎朝9:00に)

```
Hubot> おはようございます！
```

ちなみに、定期的に発言させたいだけなら、[hubot-cron](https://github.com/miyagawa/hubot-cronies)を使うという手もある。

### データの永続化を行う(brain)

hubotにデータを記憶させる機能。
[redis](http://redis.io/)が必要になる。[(redis参考)](http://ameblo.jp/principia-ca/entry-11197286812.html)

#### redisサーバの準備
homebrewでインストール

```
$ brew install redis
```

一応バージョン確認してみる。

```
% redis-server --version
Redis server v=2.8.13 sha=00000000:0 malloc=libc bits=64 build=96319fcc2102d7fa
```

Redisサーバ起動。(`Ctrl-C`で終了)

```
% redis-server /usr/local/etc/redis.conf
```

Redisサーバが自動実行されるよう登録しておくのもいいかも。

```
% ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
% launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
```

念のためプロセスを確認してみる。

```
% ps ax | grep redis-server
13443   ??  S      0:00.44 /usr/local/opt/redis/bin/redis-server 127.0.0.1:6379
13836 s005  S+     0:00.00 grep redis-server
```

これでMacを再起動しても自動でRedisサーバが起動してくれる。

自動起動をやめたいときはコチラ。

```
% launchctl unload -w ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
```

#### 実装
```hoge.coffee
module.exports = (robot) ->

  # redisに保存するためのキー
  KEY_DERBY_POINTS = 'derby_points'

  # 対象と点数を指定
  robot.hear /^(.*)さんに([0-9]+)点/, (msg) ->
    name  = msg.match[1]
    points = (robot.brain.get KEY_DERBY_POINTS) or {}
    points[name] = (points[name] or 0) + Number(msg.match[2])

    robot.brain.set KEY_DERBY_POINTS, points

    msg.send "#{name}さん: #{points[name]}点"

  # 点数の合計を表示
  robot.respond /derby list/i, (msg) ->
    points = (robot.brain.get KEY_DERBY_POINTS) or {}
    for name, point of points
      msg.send "#{name}さん: #{point}点"

  # 点数をリセット
  robot.respond /derby reset/i, (msg) ->
    robot.brain.set KEY_DERBY_POINTS, {}
    msg.send "reset: done"

```

#### 結果
```
Hubot> はらたいらさんに3000点
Hubot> はらたいらさん: 3000点
Hubot> 竹下景子さんに2000点
Hubot> 竹下景子さん: 2000点
Hubot> はらたいらさんに5000点
Hubot> はらたいらさん: 8000点
Hubot> hubot derby list
Hubot> はらたいらさん: 8000点
Hubot> 竹下景子さん: 2000点
```

### WebAPIを叩く(http)

WebAPIでデータをjsonなり何なりで取ってくる。
例として、[Google Geocoding API](https://developers.google.com/maps/documentation/geocoding/?hl=ja)を使って住所から緯度経度を取得してみる。

#### 実装
```
module.exports = (robot) ->
  robot.hear /location (.*)/, (msg) ->
    request = robot.http("https://maps.googleapis.com/maps/api/geocode/json")
                   .query(address: msg.match[1])
                   .get()
    request (err, res, body) ->
      json = JSON.parse body
      location = json['results'][0]['geometry']['location']

      msg.send "#{location['lat']}, #{location['lng']}"

```

#### 結果
```
Hubot> location 福岡市中央区大名
Hubot> 33.5869827, 130.3949105
```

### 環境変数

これだけrobotのメソッドじゃないけど、環境変数を使う方法を知っておくと設定が楽。

ローカルで環境変数を設定するには、hubot関係なくターミナルでこんな感じに。

```
$ export HUBOT_ENV_TEST_VAR=hogehoge
```

スクリプトで読み込むときはこんな感じ。

```
TEST_VAR = process.env.HUBOT_ENV_TEST_VAR
```

## まとめ
これだけできれば大抵のことはできると思う。
あとはアイデアと実装する根気があれば…！