Hubot でホームページをつくろう

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

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

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

前回は [スクレイピングする Hubot スクリプトをつくろう][hubot-adventar-2014-9] ということで、npm パッケージ `request` と `cheerio` とを使って、Adventar をスクレイピングして Advent Calendar 一覧を返す Hubot スクリプトをつくりました。

前回は HTTP リクエストしたので、今回は HTTP レスポンスしましょう (?)。 Hubot で HTTP アクセスを待ち受けて、HTML を返しましょう。要するに **Hubot でホームページ (Web サーバー) をつくりましょう**。

# スクリプトの完成イメージ

まず、今回つくろうとしている Hubot スクリプトのイメージを共有しましょう。

今回は簡単なリンク集をつくります。チャットでの実行イメージはこんな感じです。

```
bouzuya> hubot homepage add bouzuya http://bouzuya.net/
hubot> added bouzuya http://bouzuya.net/

bouzuya> hubot homepage list
hubot> [0] bouzuya http://bouzuya.net/

bouzuya> hubot homepage remove 0
hubot> removed bouzuya http://bouzuya.net/
```

また Web ブラウザで `http://<hubot url>/hubot-homepage/` を見るとリンク集が表示されます。

# さっそくつくる

まずは Hubot スクリプトのテンプレートを `generator-hubot` で生成します。

```bash
$ mkdir hubot-homepage
$ cd hubot-homepage

$ yo hubot:script
...
```

説明不要ですね。

今回は追加パッケージは不要です。 Hubot は元々 express 3.x に依存しており、HTTP の待ち受けも標準でできるようになっているからです。

さっそく書いていきましょう。

```coffeescript:src/homepage.coffee
# Description
#   A Hubot script for hosting your web page.
#
# Configuration:
#   None
#
# Commands:
#   hubot homepage list - list links in homepage
#   hubot homepage add <text> <url> - add link to homepage
#   hubot homepage remove <index> - remove link from homepage
#
# Author:
#   bouzuya <m@bouzuya.net>

module.exports = (robot) ->
  links = []

  robot.router.get '/hubot-homepage/', (req, res) ->
    li = links.map (i) ->
      """
        <li><a href="#{i.url}" target="_blank">#{i.text}</a></li>
      """
    .join '\n'
    html = """
      <html>
      <head><title>Links</title></head>
      <body>
        <h1>Links</h1>
        <ul>
        #{li}
        </ul>
      </body>
      </html>
    """
    res.type 'html'
    res.send html

  robot.respond /homepage li?st?/, (msg) ->
    msg.send links.map((i, index) -> "[#{index}] #{i.text} #{i.url}").join('\n')

  robot.respond /homepage add (.+) (https?:\/\/.+)/, (msg) ->
    text = msg.match[1]
    url = msg.match[2]
    item = { text, url }
    links.push item
    msg.send "added #{item.text} #{item.url}"

  robot.respond /homepage re?m(?:ove)? (\d+)/, (msg) ->
    index = msg.match[1]
    item = links.splice(index, 1)[0]
    msg.send "removed #{item.text} #{item.url}" if item?
```

# 解説

ええと、解説します。

## `links = []`

リンクをここに保持します。永続化はしていないため、Hubot を再起動すると消えてしまいます。

本来は Hubot の brain で永続化すべきなのですが、それはまた別の機会に紹介します。

## `robot.router`

https://github.com/github/hubot/blob/v2.10.0/src/robot.coffee#L265-L300

Node.js express に慣れたユーザーに説明するなら、これは `express()` です。`express` は Node.js のデファクトスタンダードな Web アプリケーションフレームワークです。Ruby の sinatra のような軽量のフレームワークです。

想像どおり express の解説をするときりがないので、簡単に。

`robot.router.get(path, callback)` で `GET` メソッドを待ち受けて `callback` で処理できます。`robot.router.post(path, callback)` などもあります。詳しくは [express の api reference][express-reference] を参照してください。

注意事項としては express のバージョンは 3.x なので、それに対応したドキュメントを参照し、コードを記述してください。

## `/homepage li?st?/` `/homepage re?m(?:ove)? (\d+)/`

細かいことですが、コマンドを省略できるようにしています。

`list` は `ls` でも認識されますし、`remove` は `rm` でも認識されます。

Hubot のコマンドはなるべく入力しやすく工夫すると良いと思います。

## その他

あとは特にはないですね。`list` / `add` / `remove` をそれぞれ定義し、`links` を操作できるようにしています。

# では、動かしてみましょう。

```bash
$ HUBOT_SHELL_USER_NAME='bouzuya' PATH="./node_modules/hubot/node_modules/.bin:$PATH" $(npm bin)/hubot -a shell -n hubot -r src
hubot> hubot homepage list

hubot> hubot homepage add bouzuya http://bouzuya.net/
added bouzuya http://bouzuya.net/
hubot> hubot homepage add blog http://blog.bouzuya.net/
added blog http://blog.bouzuya.net/
hubot> hubot homepage add google http://www.google.com/
added google http://www.google.com/
hubot> hubot homepage list
[0] bouzuya http://bouzuya.net/
[1] blog http://blog.bouzuya.net/
[2] google http://www.google.com/
hubot> hubot homepage rm 1
removed blog http://blog.bouzuya.net/
hubot> hubot homepage list
[0] bouzuya http://bouzuya.net/
[1] google http://www.google.com/
hubot> 
```

動きそうですね。

Web ブラウザで `http://localhost:8080/hubot-homepage/` にアクセスしてみてください。以下のような Web ページが表示されます。

![hubot-homepage.png](https://qiita-image-store.s3.amazonaws.com/0/3486/6ea6844f-9600-231b-9bd9-46a5db304103.png)

登録した links が表示されます。画像は 3 件登録した直後のものです。もちろん削除も反映されます。

# まとめ

今回は `robot.router` を使って HTTP リクエストを待ち受けてみました。

Hubot はホームページもつくれるんですね！すごい！(のか?) 

ちなみに、この機能を使って、外部サービスの Web hook へ対応したり、adapter の実装に使ったり、生成した画像を一時的にホストして返す、などができます。BOT は必ずしも HTTP の待ち受けの必要はないので、削除すべき、といった意見もありますが、互換性の問題もありますし、おそらく簡単には削除されないと思います。

ちなみに今回のサンプルは [bouzuya/hubot-homepage][] にあります。うまく動かない、などがあれば参考にどうぞ。

# 最後に

サンプルをつくるのが大変になってきました。

あとは `brain` / `adapter` くらいですかね。起動スクリプトの動きとか、スクリプト読み込みとか、Hubot 本体の動きを追っていく感じになりますかね。

次回は 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/
[github/hubot]: https://github.com/github/hubot/
[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
[ops-for-everyone]: https://speakerdeck.com/johndbritton/ops-for-everyone
[adventar]: http://www.adventar.org/
[express-reference]: http://expressjs.com/3x/api.html