「決められない」人のためのラーメン激推しBot

## 「どのお店にしよう」を解決します
突然ですが、私はラーメンが大好きで、よく食べに行っています。
ただ、生来の優柔不断な性格のせいで、いつもどのお店にいくか迷ってしまうのです。

**「誰かが自分に"ここだ！"というお店を強くプッシュしてくれたら迷わずに済むのに…」**

そこで、エリアを指定するとその付近のお店を一店だけ選んで、そのお店を"激推し"してくれるLINE Botを作ってみました！

## ラーメン激推しBot
まずはこちらをご覧ください。

https://twitter.com/kelab_8/status/1515186684606185483

おすすめのラーメン屋を紹介するLINE Botは世の中に数あれど、選ばれた一店だけをここまで激推ししてくるBotはないんじゃないでしょうか。(多分)

それでは、以下実装について解説していきます。

## 環境
Node-RED（Heroku環境上で実装）
JavaScript
[LINE Massaging API](https://developers.line.biz/ja/services/messaging-api/)
[ホットペッパーAPI](https://webservice.recruit.co.jp/doc/hotpepper/reference.html)

## 事前準備
LINE Botの作成については以下の記事を参考にしました。
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017 #nodefest](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)

また、ホットペッパーAPIについては、事前にAPIキーを払い出す必要があります。
APIリファレンスページの新規登録ボタンからメールアドレスのみ登録すれば払い出すことができるので、ハードルは低いと言えるでしょう。
（APIリファレンスページは上記の環境欄からアクセス可能です）
![画像1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/bd0911b6-8702-3440-42ef-78d2a6df3a6e.png)

## Node-REDのフロー
今回は、以下のようなフローを作成しました。
![フロー.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/200a18c1-0b8c-1ad4-7dd9-a51c7df6fa8c.png)
フローをJSON形式で出力したものは以下になります。
```json
[{"id":"7f53a09b.56753","type":"tab","label":"ラーメンBot","disabled":false,"info":""},{"id":"bf0d305b.6adfe","type":"http request","z":"7f53a09b.56753","name":"ｸﾞﾙﾒｻｰﾁAPI呼出","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key=c605504ef2fc48ad&order=4&count=20&genre=G013&keyword={{{query}}}&middle_area={{{query2}}}","tls":"","persist":false,"proxy":"","authType":"","x":400,"y":200,"wires":[["1e93ccf1.f3f843"]]},{"id":"ea2449b6.76c318","type":"function","z":"7f53a09b.56753","name":"ｸﾞﾙﾒｻｰﾁAPIﾊﾟﾗﾒｰﾀｰ設定","func":"//中エリアマスタAPIからのレスポンスデータ\nvar xmlData = msg.payload;\n//エリアコード情報を取得\nvar ereaCd = xmlData.results.middle_area[0].code.toString();\n\n//グルメサーチAPIのクエリパラメーター設定\nmsg.query = encodeURI(\"ラーメン\");\nmsg.query2 = encodeURI(ereaCd);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":190,"y":200,"wires":[["bf0d305b.6adfe"]]},{"id":"d9d384de.621088","type":"function","z":"7f53a09b.56753","name":"店名ﾒｯｾｰｼﾞ設定","func":"//グルメサーチAPIからのレスポンスデータ\nmsg.xmlData = msg.payload;\n// LINEサーバーからの内容を復元する\nmsg.payload = msg.line;\n\n//ランダムでお店を決定するための乱数生成\nmsg.num = Math.floor(Math.random() * msg.xmlData.results.shop.length);\n\n//店名のメッセージを設定\nvar replyMsg = \"僕のおすすめのお店は、\\n\" + msg.xmlData.results.shop[msg.num].name.toString() + \"\\nだよ！\";\nmsg.payload.events[0].message.text = replyMsg;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":700,"y":200,"wires":[["ea852383.71aea","e80870e6.6348b"]]},{"id":"1e93ccf1.f3f843","type":"xml","z":"7f53a09b.56753","name":"","property":"payload","attr":"","chr":"","x":550,"y":200,"wires":[["d9d384de.621088"]]},{"id":"ea852383.71aea","type":"ReplyMessage","z":"7f53a09b.56753","name":"","replyMessage":"","x":900,"y":200,"wires":[]},{"id":"b6410dd3.5a0de","type":"http in","z":"7f53a09b.56753","name":"","url":"/webhook","method":"post","upload":false,"swaggerDoc":"","x":120,"y":40,"wires":[["1cc49878.bc7048"]]},{"id":"1cc49878.bc7048","type":"function","z":"7f53a09b.56753","name":"中ｴﾘｱﾏｽﾀAPIﾊﾟﾗﾒｰﾀｰ設定","func":"//LINEサーバーからの情報を「msg.line」に退避する\nmsg.line = msg.payload;\n//後でPushでメッセージを送信するためにユーザーIDを退避する\nmsg.userID = msg.payload.events[0].source.userId;\n//送られてきたメッセージを中エリアマスタAPIのクエリパラメーターに設定\nmsg.query = encodeURI(msg.payload.events[0].message.text);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":40,"wires":[["19e632d7.16fe7d"]]},{"id":"19e632d7.16fe7d","type":"http request","z":"7f53a09b.56753","name":"中ｴﾘｱﾏｽﾀAPI呼出","method":"GET","ret":"txt","paytoqs":"ignore","url":"https://webservice.recruit.co.jp/hotpepper/middle_area/v1/?key=c605504ef2fc48ad&keyword={{{query}}}","tls":"","persist":false,"proxy":"","authType":"","x":550,"y":40,"wires":[["37148764.4c5268"]]},{"id":"37148764.4c5268","type":"xml","z":"7f53a09b.56753","name":"","property":"payload","attr":"","chr":"","x":710,"y":40,"wires":[["a84c1efb.c869a"]]},{"id":"6beb2c4f.e39834","type":"function","z":"7f53a09b.56753","name":"店ｱｸｾｽﾒｯｾｰｼﾞ設定","func":"//店の場所のメッセージ設定\nvar pushMsg = \"お店の場所は、\\n\" + msg.xmlData.results.shop[msg.num].access.toString() + \"\\nだね！\";\nmsg.payload = pushMsg;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":370,"y":280,"wires":[["8c299d54.206ac","962d070.92463f8"]]},{"id":"e80870e6.6348b","type":"delay","z":"7f53a09b.56753","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":190,"y":280,"wires":[["6beb2c4f.e39834"]]},{"id":"8c299d54.206ac","type":"PushMessage","z":"7f53a09b.56753","name":"","x":580,"y":280,"wires":[]},{"id":"962d070.92463f8","type":"delay","z":"7f53a09b.56753","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":190,"y":360,"wires":[["66e36af5.25f8a4"]]},{"id":"66e36af5.25f8a4","type":"function","z":"7f53a09b.56753","name":"ｲﾒｰｼﾞ送信ﾊﾟﾗﾒｰﾀｰ設定","func":"//イメージをPUSH送信するためのパラメーター設定\nmsg.url=`https://api.line.me/v2/bot/message/push`;\nmsg.method=`POST`;\nmsg.headers={\n   \"Authorization\":\"Bearer s1E2zr4Zl8QpH4vXfEORPB8ufQA3bbJm96XCThSztXlarg3H7+na4JdZo/D3eVn0pf7XVovWL8CeyifVv2RD15rXfkgONYF7sVnmiS4ztZm3G6Bug7BFhaCxHBtDaFkHxIX/96K8p3tcZeGT67U8CAdB04t89/1O/w1cDnyilFU=\",\n   \"Content-Type\":\"application/json\"\n};\n\nmsg.payload={ \n\t\"to\":msg.userID,\n\t\"messages\":[\n\t\t{\n           \"type\":\"image\",\n           \"originalContentUrl\":msg.xmlData.results.shop[msg.num].photo[0].pc[0].l.toString(),\n           \"previewImageUrl\":msg.xmlData.results.shop[msg.num].photo[0].pc[0].l.toString()\n       }\n\t]\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":380,"y":360,"wires":[["c771c58d.353868"]]},{"id":"28acc6e5.5e6c5a","type":"delay","z":"7f53a09b.56753","name":"","pauseType":"delay","timeout":"500","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":190,"y":440,"wires":[["7b717fec.dc599"]]},{"id":"7b717fec.dc599","type":"function","z":"7f53a09b.56753","name":"ﾗｽﾄﾒｯｾｰｼﾞ設定","func":"//メッセージ設定\nvar pushMsg = \"わぁ～、おいしそう！\"\nmsg.payload = pushMsg\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":440,"wires":[["a74c1ef3.eedc6"]]},{"id":"a74c1ef3.eedc6","type":"PushMessage","z":"7f53a09b.56753","name":"","x":540,"y":440,"wires":[]},{"id":"c771c58d.353868","type":"http request","z":"7f53a09b.56753","name":"ｲﾒｰｼﾞ送信","method":"use","ret":"txt","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":560,"y":360,"wires":[["28acc6e5.5e6c5a"]]},{"id":"a84c1efb.c869a","type":"switch","z":"7f53a09b.56753","name":"ﾚｽﾎﾟﾝｽ判定","property":"payload.results.results_available[0]","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":150,"y":120,"wires":[["568d0643.58e178"],["ea2449b6.76c318"]]},{"id":"4ee56df4.f5f3d4","type":"ReplyMessage","z":"7f53a09b.56753","name":"","replyMessage":"ごめんね、場所がわからなかったよ…。","x":480,"y":120,"wires":[]},{"id":"568d0643.58e178","type":"function","z":"7f53a09b.56753","name":"0件処理","func":"//LINEサーバーからの内容を復元する\nmsg.payload = msg.line\nvar replyMsg = \"ごめんね、場所がわからなかったよ…。\"\nmsg.payload.events[0].message.text = replyMsg\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":120,"wires":[["4ee56df4.f5f3d4"]]}]
```

## フローの中身、ソースコード
では、ここからフローの中身について順番に解説していきます。

### 中エリアマスタAPIパラメーター設定
![フロー1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/547bba83-1c39-ee85-7b6d-da0e9bc207e9.png)
今回使用しているホットペッパーAPIで場所をキー情報にお店の情報を抽出するためには、エリアコードをパラメーターとして設定する必要があるのですが、そのエリアコードを特定するために、まず「中エリアマスタAPI」を呼び出します。
中エリアマスタAPIのパラメーターには、LINEから送信されてきた文言をそのまま設定します。
```js
//LINEサーバーからの情報を「msg.line」に退避する
msg.line = msg.payload;
//後でPushでメッセージを送信するためにユーザーIDを退避する
msg.userID = msg.payload.events[0].source.userId;
//送られてきたメッセージを中エリアマスタAPIのクエリパラメーターに設定
msg.query = encodeURI(msg.payload.events[0].message.text);

return msg;
```
中エリアマスタAPIの呼び出し方は以下の通りです。
パラメーターとしてAPIキーと検索キーワードを設定しています。
```
https://webservice.recruit.co.jp/hotpepper/middle_area/v1/?key={{{APIkey}}}&keyword={{{query}}}
```

### 0件処理
![フロー2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/ac70e8b1-f4cd-c077-2eda-49a38cac5c91.png)
中エリアマスタAPIから返ってきたエリアコードを、次のグルメサーチAPIのパラメーターに使用する訳ですが、ユーザーが指定したエリアが存在しないことも考えられます。
その時は、以下のような0件処理に流して会話を終了させます。
```js
// LINEサーバーからの内容を復元する
msg.payload = msg.line;
var replyMsg = "ごめんね、場所がわからなかったよ…。";
msg.payload.events[0].message.text = replyMsg;

return msg;
```
### グルメサーチAPIパラメーター設定
![フロー3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/c486c164-4003-4545-7578-92229bbdb20c.png)
エリアコードが取得できた場合は、それを改めてクエリパラメーターとして設定します。
```js
//中エリアマスタAPIからのレスポンスデータ
var xmlData = msg.payload;
//エリアコード情報を取得
var ereaCd = xmlData.results.middle_area[0].code.toString();

//グルメサーチAPIのクエリパラメーター設定
msg.query = encodeURI("ラーメン");
msg.query2 = encodeURI(ereaCd);

return msg;
```
グルメサーチAPIの呼び出し方はこちら。
パラメーターは以下の通りです。
- key：APIキー
- order：4(人気順)
- count：20(最大抽出件数20件)
- genre：G013(ラーメン店)
- keyword：検索ワード(今回は"ラーメン")
- middle-area：中エリアコード(中エリアマスタAPIの抽出結果)
```
https://webservice.recruit.co.jp/hotpepper/gourmet/v1/?key={{{APIkey}}}&order=4&count=20&genre=G013&keyword={{{query}}}&middle_area={{{query2}}}
```

### 店名メッセージ設定
![フロー4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/90f5acdd-567b-8e7f-ad4f-759460e60f3d.png)
グルメサーチAPIの結果からリプライする店名を設定します。
今回はお店をランダムに選ぶというコンセプトのため、乱数を生成してAPIの結果を格納したオブジェクトからランダムで要素を抜き出しています。
```js
//グルメサーチAPIからのレスポンスデータ
msg.xmlData = msg.payload;
// LINEサーバーからの内容を復元する
msg.payload = msg.line;

//ランダムでお店を決定するための乱数生成
msg.num = Math.floor(Math.random() * msg.xmlData.results.shop.length);

//店名のメッセージを設定
var replyMsg = "僕のおすすめのお店は、\n" + msg.xmlData.results.shop[msg.num].name.toString() + "\nだよ！";
msg.payload.events[0].message.text = replyMsg;

return msg;
```

### 店アクセスメッセージ設定
![フロー5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/6d633b1d-fad1-fbfc-c247-23a1fc4086ef.png)
続いてお店のアクセス情報も送信します。
ちなみに、メッセージを送信する前に0.5秒ディレイさせるノードを設けているのは、そうしないとたまにメッセージの順序が逆転してしまうことがあるためです。
```js
//店の場所のメッセージ設定
var pushMsg = "お店の場所は、\n" + msg.xmlData.results.shop[msg.num].access.toString() + "\nだね！";
msg.payload = pushMsg;
return msg;
```

### イメージ送信パラメーター設定
![フロー6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/b3e27881-3c59-6980-cf10-f6a175c42dbb.png)
その後、お店の画像を送信します。これまでと同じようにPushMassageノードを使いたかったのですが、なぜかイメージのプッシュ送信だけうまくいかなかったので、今回はJSON形式でパラメーター設定してhttp requestノードでPOSTする方法をとりました。
```js
//イメージをPUSH送信するためのパラメーター設定
msg.url=`https://api.line.me/v2/bot/message/push`;
msg.method=`POST`;
msg.headers={
   "Authorization":"Bearer {{{ChannelAccessToken}}}",
   "Content-Type":"application/json"
};

msg.payload={ 
	"to":`U045897e2d966c93ff2c4fd46ad0fb409`,
	"messages":[
		{
           "type":"image",
           "originalContentUrl":msg.xmlData.results.shop[msg.num].photo[0].pc[0].l.toString(),
           "previewImageUrl":msg.xmlData.results.shop[msg.num].photo[0].pc[0].l.toString()
       }
	]
};

return msg;
```

### ラストメッセージ設定
![フロー7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607554/6ea2364b-714a-66aa-6e51-6ac1a999c67e.png)
最後にダメ押しのメッセージを送って完了です！
```js
//メッセージ設定
var pushMsg = "わぁ～、おいしそう！"
msg.payload = pushMsg
return msg;
```

## あとがき
ラーメンやつけ麺、魚介系や家系といった種類で絞ったりできても良いなとは思ったのですが、ユーザーのお店との一期一会を楽しんでもらいたいという想いもあり、場所だけを指定してお店はランダムで選ばれる形にしました。
今度実際にこれで選ばれたお店に食べに行ってみたいと思います！

あとこれは余談ですが、このBotのデバッグをしている時おいしそうなラーメンの画像を見すぎて気が狂いそうになりました…。

以上、ここまでお読みいただきありがとうございました！