楽天のAPIを使ってマスクを買えるBotを作ってみた[LINEBotリッチメニュー]

##はじめに

前回の[楽天市場のAPIを使って買えるマスクを提案してくれるLINEBotを作ってみた \- Qiita](https://qiita.com/3yaka4/items/87d2b2303267eed12e14)をブラッシュアップしました。

##概要
前回できたものは、
LINEBotにほしいもの、「マスク」と入力したら
**楽天市場から「マスク」を検索**して、絞り込み検索で、**購入可能**、**最安価でソート**、**最低金額○円以上**、**最高金額○円以下**の商品を商品画像付きで３商品くらい返してくれるBot
結果として出来上がったものの、楽天の商品名ってSEO対策のせいでやたらと長くてかなりエリアを取る、３つ出したのにとても見比べられず実用性が低かったのでリッチメニューを使って使いやすくしようと思いました。

### 作りたいもの
マスクにセグメントされた楽天市場の商品を３つ引っ張ってきて
1. カルーセルパネルで開く
2. 複数検索
3. 商品名を３行くらいに収める
4. 金額かレビューを出す
5. （マスクは）怪しいショップが多かったので店舗名を出す
6. ngrokで動かしているので何かにデプロイする

### 作れなかったところ
1. ○　→　カルーセルパネルで開く
2. ○　→　複数検索してるっぽいみため
3. ○　→　商品名を３行くらいに収める
4. ○　→　金額かレビューを出す
5. ○　→　（マスクは）怪しいショップが多かったので店舗名を出す
6. ×　→　ngrokで動かしているのでHerokuにデプロイする

###環境
Node.js v13.7.0
MacBook Pro macOS Mojave
Visual Studio Code v1.43.1

###できたもの
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">買えるマスクLINEBot♡<br>リッチメニュー使った♡カルーセルしたった！<br>それにしてもマスクの値段、いつまでこんなに高いのしかないんだろう。。<br>家庭に2枚届いてもな。。。<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a> <a href="https://t.co/M1vEMeJyp2">pic.twitter.com/M1vEMeJyp2</a></p>&mdash; 3yaka (@3yaka4) <a href="https://twitter.com/3yaka4/status/1245351558377660417?ref_src=twsrc%5Etfw">April 1, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

「マスクある？」と質問することで、楽天市場の中で在庫あり商品、最安価の商品名と商品URLと商品画像のあるマスクをカルーセルパネルを使って横スクロールで商品を提案してくれます。

---------------------------------------
#### カルーセルパネルに入れる情報
楽天市場API↓を使って

```
https://app.rakuten.co.jp/services/api/IchibaItem/Search/20170706?format=json&rakutenAPiurl&keyword=mask&genreId=501143&availability=1&sort=%2BitemPrice&minPrice=1000&maxPrice=4000&carrier=2&hits=3&applicationId=***
```
[![Image from Gyazo](https://i.gyazo.com/fc2ff74b8e4e8becd4c64fe31e679e56.jpg)](https://gyazo.com/fc2ff74b8e4e8becd4c64fe31e679e56)
上記のカルーセルに入れる情報を作ります。

```
//APIが深いので前半をまとめる
    const item = items[i].Item;
//商品画像（中サイズ）
    const itemimg = `${item.mediumImageUrls[0].imageUrl}`;
//商品URL
    const producturl = `${item.itemUrl}`;
//商品名
    const productlabellg = `${item.itemName}`;
//商品名を54文字以下にして...をつける
    const productlabel = productlabellg.slice(0, 54) + "...";
//商品金額
    const productprice = `¥${item.itemPrice}`;
//レビュー平均値
    const producavg = `${item.reviewAverage}`;
//ショップ名
    const productshop = `${item.shopName}`;
//　LINEに入力した文字
    const own = `${ownword}`;

```

これを使って、カルーセルパネルに設定していく
[![Image from Gyazo](https://i.gyazo.com/e86340a23b617378395b91586cb95934.jpg)](https://gyazo.com/e86340a23b617378395b91586cb95934)

```
let columns = [];

let carousel =
    {
      "thumbnailImageUrl": itemimg,
      "imageBackgroundColor": "#FFFFFF",
      "title": own,
      "text": productlabel,
      "defaultAction": {
        "type": "uri",
        "label": "View detail",
        "uri": producturl
      },
      "actions": [
        {
          "type": "message",
          "label": productprice,
          "text": productprice
        },
        {
          "type": "uri",
          "label": productshop,
          "uri": producturl
        }
      ]
    };
    columns.push(carousel);
  }
```
これを３つ分作りたいのでFor文の中に入れちゃいます。

### 全コード(ngrok)
``` node.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;
const rakutenAPiurl = "https://app.rakuten.co.jp/services/api/IchibaItem/Search/20170706?format=json";
const rakutenAppId = '***';
const guidecat = 'https://i.gyazo.com/97840790b257952c89c59e7c176e114c.png';

const config = {
  channelSecret: '***',
  channelAccessToken: '***'
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
  console.log(req.body.events);
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = event.message.text;
  if (mes.indexOf('？') > -1) {
    getNodeVer(event.source.userId, mes);
    return client.replyMessage(event.replyToken, [{
      type: 'image',
      originalContentUrl: guidecat,
      previewImageUrl: guidecat
    },
    { type: "text", text: '在庫があって安いのはこれだよ〜' }]);

  } else {
    // mes = event.message.text;

    return client.replyMessage(event.replyToken, [{
      type: 'image',
      originalContentUrl: guidecat,
      previewImageUrl: guidecat
    },
    { type: "text", text: '〇〇ある？って聞いてほしいな' }]);
  }

}

const getNodeVer = async (userId, mes) => {

  let ownword = mes.split('ある？');
  console.log('mes->' + mes + 'ownword->' + ownword);
  const res = await axios.get(rakutenAPiurl + '&keyword=' + encodeURIComponent(ownword) + '&genreId=501143&availability=1&sort=%2BitemPrice&minPrice=1000&maxPrice=4000&carrier=2&hits=3&applicationId=' + rakutenAppId);
  const items = res.data.Items;
  let columns = [];

  for (let i = 0, len = items.length; i < len; i++) {
    const item = items[i].Item;
    const itemimg = `${item.mediumImageUrls[0].imageUrl}`;
    const producturl = `${item.itemUrl}`;
    const productlabellg = `${item.itemName}`;
    const productlabel = productlabellg.slice(0, 54) + "...";
    const productprice = `¥${item.itemPrice}`;
    const producavg = `${item.reviewAverage}`;
    const productshop = `${item.shopName}`;
    const own = `${ownword}`;

    let carousel =
    {
      "thumbnailImageUrl": itemimg,
      "imageBackgroundColor": "#FFFFFF",
      "title": own,
      "text": productlabel,
      "defaultAction": {
        "type": "uri",
        "label": "View detail",
        "uri": producturl
      },
      "actions": [
        {
          "type": "message",
          "label": productprice,
          "text": productprice
        },
        {
          "type": "uri",
          "label": productshop,
          "uri": producturl
        }
      ]
    };
    columns.push(carousel);
  }

 await client.pushMessage(userId, {
    "type": "template",
    "altText": "this is a carousel template",
    "template": {
      "type": "carousel",
      "columns": columns,
      "imageAspectRatio": "rectangle",
      "imageSize": "cover"
    }
  });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

```
このコードをngrokで動かすと、動きます！

しかしこれをHerokuに持っていくと失敗。。。
課題なのでタイムオーバー。。。
Herokuにはdeployできてるのになぁ。。。
何が起こっているのかな

[![Image from Gyazo](https://i.gyazo.com/f1d0a356b66e96b99c723b4656bef71d.jpg)](https://gyazo.com/f1d0a356b66e96b99c723b4656bef71d)

Herokuを有料にしないといけないとかmにつけたのですが、Heroku側のSSL認証問題なのでしょうか。
[参考URL]
Heroku SSL
https://devcenter.heroku.com/articles/ssl

###[now](#now)
#### Herokuは諦めてnowで試してみた


[1時間でLINE BOTを作るハンズオン](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)をベースにしたけどうまくいかなかったのでProtoOutの校長先生がさらに書いていただいて、
[LINE BOTで天気を返すサンプルがngrokで動いてnowで動かない件](https://qiita.com/n0bisuke/items/fb19ed3cd0138135ae69)を参考に書き換えたら動きましたー！！！！

```node.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;
const rakutenAPiurl = "https://app.rakuten.co.jp/services/api/IchibaItem/Search/20170706?format=json";
const rakutenAppId = '***';
const guidecat = 'https://i.gyazo.com/97840790b257952c89c59e7c176e114c.png';

const config = {
  channelSecret: '***',
  channelAccessToken: '***'
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
  console.log(req.body.events);
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result));
});

const client = new line.Client(config);

async function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let mes = event.message.text;
  let linemes = mes.indexOf('ある？');

  if ( linemes < 0) {
    return client.replyMessage(event.replyToken, [{
      type: 'image',
      originalContentUrl: guidecat,
      previewImageUrl: guidecat
    },
    { type: "text", text: '〇〇ある？って聞いてほしいな' }]);
  }

  await client.replyMessage(event.replyToken, [{
    type: 'image',
    originalContentUrl: guidecat,
    previewImageUrl: guidecat
  },
    {
    type: "text", text: '在庫があって安いのはこれだよ〜' }]);
    return getRakuten(event.source.userId, mes);
}
const getRakuten = async (userId, mes) => {

  let ownword = mes.split('ある？');
  console.log('mes->' + mes + 'ownword->' + ownword);
  const res = await axios.get(rakutenAPiurl + '&keyword=' + encodeURIComponent(ownword) + '&genreId=501143&availability=1&sort=%2BitemPrice&minPrice=1000&maxPrice=4000&carrier=2&hits=3&applicationId=' + rakutenAppId);
  const items = res.data.Items;
  let columns = [];
  for (let i = 0, len = items.length; i < len; i++) {
    const item = items[i].Item;
    const msg = `${item.itemName}\n ${item.itemUrl}\n¥${item.itemPrice}`;
    const itemimg = `${item.mediumImageUrls[0].imageUrl}`;
    const producturl = `${item.itemUrl}`;
    const productlabellg = `${item.itemName}`;
    const productlabel = productlabellg.slice(0, 54) + "...";
    const productprice = `¥${item.itemPrice}`;
    const producavg = `${item.reviewAverage}`;
    const productshop = `${item.shopName}`;
    const own = `${ownword}`;

    let carousel =
    {
      "thumbnailImageUrl": itemimg,
      "imageBackgroundColor": "#FFFFFF",
      "title": own,
      "text": productlabel,
      "defaultAction": {
        "type": "uri",
        "label": "View detail",
        "uri": producturl
      },
      "actions": [
        {
          "type": "message",
          "label": productprice,
          "text": productprice
        },
        {
          "type": "uri",
          "label": productshop,
          "uri": producturl
        }
      ]
    };
    columns.push(carousel);

  }//for

  return client.pushMessage(userId, {
      "type": "template",
      "altText": "this is a carousel template",
      "template": {
        "type": "carousel",
        "columns": columns,
        "imageAspectRatio": "rectangle",
        "imageSize": "cover"
      }
    });
}

(process.env.NOW_REGION) ? module.exports = app : app.listen(PORT);
console.log(`Server running at ${PORT}`);


```


### 感想
やり進めていくとFlex Messageの存在を知りました。。。こっちの方がやりたかった。。。
次はこれをやろう。

Deployはやっぱりハマってしまった。
頑張ろうー。

### 参考サイト
[LINE Messaging APIのテンプレートメッセージをまとめてみる \| Developers\.IO](https://dev.classmethod.jp/articles/line-messaging-api/)
[LINEのBot開発 超入門（前編） ゼロから応答ができるまで \- Qiita](https://qiita.com/nkjm/items/38808bbc97d6927837cd)