hubot-line を使って晩ごはんの献立を考えてくれる LINE BOT を作ってみた

# 注意
以下の文章は2016年上半期に利用可能だった旧 API トライアルアカウント (LINE BOT API Trial Account) についての内容です。

このトライアルアカウントは 2016/11/16 に削除され、今後使用できなくなります。以降は Developer Trial にて新 API を利用してください。

また、 Developer Trial では今のところ下記コードそのままでは動かないため本記事は参考程度に留めておいて下さい。

-- 注意ここまで --

## はじめに

はじめまして、[sfus](http://sfus.net)です。

以前から Slack で献立提案 Bot を個人用に作ろうとしていましたが、ちょうど LINE BOT API Trial Account が公開されたので、Slack BOT ではなく LINE BOT で運用してみることにしてみました。

LINE BOT API はオフィシャルで[各言語の SDK](https://github.com/line) が公開されていますが、今回はそちらではなく GitHub 社製 Bot フレームワーク [HUBOT](https://hubot.github.com/) とそのアダプターの [hubot-line](https://github.com/umakoz/hubot-line) を使って作成してみます。

hubot-line については同僚で作者の @umakoz さんが以下の記事に一通り説明されていますのでこちらをご参考ください。

[LINE BOT API を Hubot から使える hubot-line を作った - Qiita](http://qiita.com/umakoz/items/6a0f36c9fc4eda1e839b)

## 事前準備

まず、[LINE BUSINESS CENTER](https://business.line.me) から LINE BOT API Trial Account に申し込みます。
2016/05/05 現在、初回1万人の募集が締め切られた後の、追加募集中なのでまだ作成されていない方はお早めにどうぞ。

[【LINE】「LINE BOT API Trial Account」の追加募集を開始 | LINE Corporation | ニュース](https://linecorp.com/ja/pr/news/ja/2016/1340)


そして hubot-line を使ったサンプルも @umakoz さんが作ってくれているので、こちらを参考に開発していきます。

[umakoz/hubot-line-example: Example code for hubot-line.](https://github.com/umakoz/hubot-line-example)

このサンプルは Heroku 上で動作させるものになっているので、Heroku アカウントが無い場合は登録しておきます。

[Heroku: Cloud Application Platform](https://www.heroku.com/)

そして LINE BOT API は Server IP Whitelist に接続元 IP の指定が必要なため、Heroku アプリケーションの IP を固定するために [Fixie](https://elements.heroku.com/addons/fixie) というプロキシを使って対応します。Fixie は月500リクエストまでは無料で使えますが、その場合でも利用時にクレジットカードの登録が必要になるのでご注意ください。

参考：[LINE BOT をとりあえずタダで Heroku で動かす - Qiita](http://qiita.com/yuya_takeyama/items/0660a59d13e2cd0b2516) 

## hubot-line-example の動作手順

[hubot-line-example](https://github.com/umakoz/hubot-line-example) を git clone してローカルに持ってきます。

Heroku 上のセットアップの方法も全部 [Readme](https://github.com/umakoz/hubot-line-example) に書いて頂いているのでそのまま実施します。

### Heroku アプリケーション作成

```
$ heroku apps:create
```

（必要があればアプリケーション名の変更も）

### Heroku の環境変数設定

```
$ heroku config:add HUBOT_LINE_CHANNEL_ID="your_channel_id"
$ heroku config:add HUBOT_LINE_CHANNEL_SECRET="your_channel_secret"
$ heroku config:add HUBOT_LINE_CHANNEL_MID="your_channel_mid"
```

ここに設定する値は、[LINE BUSINESS CENTER](https://business.line.me) の「あなたのアカウント」ページの「使う」を押した先の [LINE developers](https://developers.line.me/channels/) のページから確認することができます。

![LINE_Business_Center.png](https://qiita-image-store.s3.amazonaws.com/0/46167/a797f582-c85d-6c87-5dcf-3cca0cc72249.png "LINE_Business_Center.png")

![LINE_developers.png](https://qiita-image-store.s3.amazonaws.com/0/46167/be0d4de7-7071-751e-4a9c-5725287a4d3d.png "LINE_developers.png")

動作確認用にログレベルを上げておきます。

```
$ heroku config:add HUBOT_LOG_LEVEL="debug"
```

動作ログは `heroku logs --tail` で確認できます。

※ ちなみにこちらのアプリアイコンはめし丸くんになります。本 Bot は個人用途に限定していますが、権利者の方で問題等あればお知らせください。福岡県産米おいしいよ！（ステマ）

[金のめし丸くん｜JA全農ふくれん めし丸くん](http://www.meshimaru.com/)

### Heroku アドオン追加

Redis と Fixie アドオンを入れます（今回のサンプルでは Redis は使っていませんが）

```
$ heroku addons:create rediscloud:30
$ heroku addons:create fixie:tricycle
```

### LINE BOT 設定

Fixie 追加時に表示された IP アドレスを2つを、 [LINE developers](https://developers.line.me/channels/) の Server IP Whitelist に設定します。（下図は追加後の状態）

![LINE_developers_WL.png](https://qiita-image-store.s3.amazonaws.com/0/46167/7a73779e-4dce-a25a-170d-bff84cd318bd.png "LINE_developers_WL.png")

この IP アドレスは Heroku の管理画面から辿れる Fixie のページから確認することもできます。

そして、[LINE developers](https://developers.line.me/channels/) の Basic Information の下の方にある Edit ボタンからこの Heroku アプリの URL を Callback URL に設定します。（この Heroku アプリの情報は `heroku apps:info` で確認できます）

```
https://your-app-name.herokuapp.com:443/hubot/line/callback
```

![LINE_developers_BI.png](https://qiita-image-store.s3.amazonaws.com/0/46167/ecc216e5-d44a-419e-5829-ff3b9681952a.png "LINE_developers_BI.png")

### hubot-line-example 設定

必要に応じて、 `./scripts/line.coffee` ファイルの以下の設定を自分の Heroku アプリの URL に書き換えておきます。（画像などのバイナリデータをやり取りするための設定です。今回のサンプルでは使用していません）

```coffeescript:./scripts/line.coffee
contentEndpoint = 'https://your-app-name.herokuapp.com:443/download'
```

### Heroku デプロイ＆動作確認

```
$ git add .
$ git commit -m 'set contentEndpoint.'
$ git push heroku master
```

（remote が設定されていない人は `git remote add heroku https://git.heroku.com/your-app-name.git` を push 前に実施してください）

あとは、 [LINE developers](https://developers.line.me/channels/) の QRコードから自分の Bot をお友達に追加して、`badger` と話しかけたら返事をしてくれるはずです。

![IMG_2047.jpg](https://qiita-image-store.s3.amazonaws.com/0/46167/ba63f408-50e2-6090-5ac1-25fac454a3a9.jpeg "IMG_2047.jpg")

（スタンプや位置情報を送っても返事をしてくれますが、スタンプについてはデフォルトで入っているような特定のスタンプしか使えません。位置情報についても、サンプルbotは本文が空だと返事してくれないので、現在位置から動かさずに住所文字列が送られるようにすれば返事が来ます）

## 献立提案Botの実装

前フリが長かったですが、サンプル Bot の動作確認ができたら、Hubot アプリの実装を進めていきます。（Hubot アプリの作り方についてはここでは省略します）

献立データをどこから取ってくるか悩みましたが、最大手の[クックパッド](http://cookpad.com/)さんは API を公開していない（＆[クローラは迷惑をかける](http://techlife.cookpad.com/entry/2015/02/24/161915))ので、[楽天レシピ](http://recipe.rakuten.co.jp/)の検索APIを公開してくれている[楽天ウェブサービス](http://webservice.rakuten.co.jp/)を使わせていただくことにします。

API を利用するため、事前に楽天ウェブサービスの利用登録を行っておいてください。

[楽天ウェブサービス(RAKUTEN WEBSERVICE)](http://webservice.rakuten.co.jp/)

### サンプルソース

Hubot の実装ですが、楽天レシピの WebService API を調べていて以下の投稿を知ったので、こちらのクローンを作ってみようと思います。（表題もこのエントリを参考にしています）

[晩ごはんの献立を一緒に考えてくれるbotを作ってみた - Qiita](http://qiita.com/nwatanabe/items/7b30de9e7402d3589d00)

上記エントリでは Slack + Ruboty(Ruby) での実装になっていますが、だいたい似たような作りになると思います。

以下、サンプルソースです。

```coffeescript:./scripts/line.coffee
{LineRawMessageListener, LineImageListener, LineVideoListener, LineAudioListener, LineLocationListener,
LineStickerListener, LineContactListener, LineRawOperationListener, LineFriendListener, LineBlockListener,
LineTextAction, LineImageAction, LineVideoAction, LineAudioAction, LineLocationAction, LineStickerAction
} = require 'hubot-line'

_ = require 'underscore';

applicationId = process.env.HUBOT_LINE_APPLICATION_ID

categoryEndpoint = "/services/api/Recipe/CategoryList/20121121?format=json&formatVersion=2&elements=categoryName%2CcategoryId%2CparentCategoryId&categoryType=medium&applicationId=#{applicationId}"
recipeEndpoint = (categoryId) -> "/services/api/Recipe/CategoryRanking/20121121?format=json&formatVersion=2&categoryId=#{categoryId}&applicationId=#{applicationId}"

categoryMap = {}

module.exports = (robot) ->
  createCategoryMap(robot)

  robot.respond /(?:(?:今日|きょう)(?:は|も)、?)?(.+)/, (msg) ->
    foodName = msg.match[1].replace(/かな$/, '')

    if foodName?.match(/(何|なん)でもいい/)
      categoryId = categoryMap[_.sample(Object.keys(categoryMap))]
      callRecipeServiceApi msg, recipeEndpoint(categoryId), (results) ->
        recipe = _.sample(results)
        msg.send recommendRecipe(foodName, "なら", recipe)

    else if categoryMap[foodName]?
      categoryId = categoryMap[foodName]
      callRecipeServiceApi msg, recipeEndpoint(categoryId), (results) ->
        recipe = _.sample(results)
        msg.send recommendRecipe(foodName, "だと", recipe)
    else
      msg.send recommendFood(categoryMap, foodName)

createCategoryMap = (robot) ->
  callRecipeServiceApi robot, categoryEndpoint, (results) ->
    for cat in results.medium
      categoryMap[cat.categoryName] = "#{cat.parentCategoryId}-#{cat.categoryId}"
    console.log("category: #{JSON.stringify(categoryMap)}")

callRecipeServiceApi = (robot, apiPath, callback) ->
  robot.http("https://app.rakuten.co.jp" + apiPath)
    .header('Accept', 'application/json')
    .get() (err, res, body) ->
      if err
        if res.statusCode is 503
          robot.send "今ちょっと忙しいみたい。またあとで話しかけてみてね"
        else
          robot.send "うまく処理できなかったみたい。ごめんね\n(ERROR: #{err} (#{res.statusCode}))"
        console.log "Error Occurred: #{res.statusCode}: #{err}"
        return
      if res.statusCode isnt 200
        robot.send "うまく処理できなかったみたい。ごめんね\n(STATUS: #{res.statusCode})"
        console.log "Bad Response: #{res.statusCode}: #{body}"
        return

      return callback(JSON.parse(body).result)

recommendRecipe = (foodName, phrase, recipe) ->
  """
  #{foodName}#{phrase}、#{recipe.recipeTitle} とかはどう？
  #{recipe.recipeUrl}
  """

recommendFood = (categoryMap, foodName) ->
  matchCat = Object.keys(categoryMap).filter((cat) ->
    cat.match(foodName))

  if not matchCat? or matchCat.length is 0
    return "#{foodName} だとわからないからもうちょっと詳しく教えて"
  else if matchCat.length is 1
    return "#{foodName}って、#{matchCat[0]} のこと？"
  else
    return "#{foodName}って、#{matchCat.join("、")} のどれのこと？"
```

### ライブラリの追加

上記ソースで利用しているライブラリを package.json に追加しておきます。

```
$ npm install --save underscore
```

### 楽天ウェブサービスのアプリケーションIDの確認＆設定

[楽天ウェブサービス: アプリ一覧](https://webservice.rakuten.co.jp/app/list) からアプリケーションIDをコピーしておきます。

![rakuten_webservice.png](https://qiita-image-store.s3.amazonaws.com/0/46167/952acb34-eae2-8521-6fc8-f2ea16ebbd18.png "rakuten_webservice.png")

### Hubot のローカルテスト

上記実装では HUBOT_LINE_APPLICATION_ID という環境変数を使っているので、コピーしたアプリケーションIDを設定しておきます。

```
$ export HUBOT_LINE_APPLICATION_ID=XXXXXXXXXXXXXXXXXXX
$ bin/hubot
> きょうはステーキかな
> ステーキだと、バルサミコ醤油ソース de 絶品ポークソテー とかはどう？
http://recipe.rakuten.co.jp/recipe/1570007081/
```

### Heroku デプロイ＆動作確認

Heroku 側にも環境変数を設定しておきます。

```
$ heroku config:add HUBOT_LINE_APPLICATION_ID="XXXXXXXXXXXXXXXXXXX"
$ git add .
$ git commit -m 'Change hubot logic to use Rakuten recipe API'
$ git push heroku master
```

![IMG_2046.jpg](https://qiita-image-store.s3.amazonaws.com/0/46167/0cdd9df9-13f5-74c3-f7a6-2762bb92d77a.jpeg "IMG_2046.jpg")

## 感想

Heroku も Hubot も CoffeeScript も慣れてはいませんでしたが、hubot-line と hubot-line-example のおかげで簡単に LINE BOT を作ることができました。（作者の @umakoz さんありがとうございます）

現在、Trial Account でも[申請](https://feedback.line.me/enquete/public/915-RRUi5HII)すれば最大5000人までお友達登録ができるということなので（デフォルトは50人）、個人でも色々面白い使い方ができるんじゃないかなと思っています。

ちなみに、もともとは家族のグループチャットに Bot を住まわせようとしたのですが（翻訳 Bot のように）、現在公開されている LINE BOT API のアカウントではグループチャットに追加することができないみたいなので、家族での使い方についてはちょっと考え中です。

また、末尾に URL を書いていてもサムネイルが勝手には表示されないみたいなのでその辺りは改良しようかなと思っています。

なお、本サンプルソースは hubot-line-example と同じく実運用を想定していないので、不特定多数に公開する場合には別途構成をご検討ください。

参考：[大量メッセージが来ても安心なLINE BOTサーバのアーキテクチャ - Qiita](http://qiita.com/yoichiro@github/items/6d4c7309210af20a5c8f)

それでは素敵なBOT LIFEを！