LINE Botでもコンビニの新商品を知りたい

偉大なる先人に敬意を表して。
[GASでセブンイレブンの今週の新商品をスクレイピングして通知する](https://qiita.com/khsk/items/a3d942a2d091abed7c4e)
[セブンだけでなくファミマの今週の新商品もスクレイピングしてみた](https://qiita.com/uhooi/items/08828a82a30173d0286d)

最近LINE Botがちょっとしたマイブームなので、GASと[LINE Messaging API](https://developers.line.me/ja/services/messaging-api/)でつくってみることにしました。
ただ移植するだけだと芸がないので、追加でローソンとミニストップに対応しています。

# LINE Messaging API

LINE Messaging APIにはいくつかのメッセージタイプが用意されていて、作りたいアプリに応じて最適なUIを選べるのが一つの魅力だと思います。
今回は、[テンプレートメッセージ](https://developers.line.me/ja/docs/messaging-api/message-types/#template-messages)の1つであるカルーセルテンプレートを使いました。
カラム数が10個までっていうのと、画像に以下の制約があるのが注意点。

> 画像URL（最大文字数：1000）
> HTTPS
> JPEGまたはPNG
> 縦横比：1:1.51
> 最大横幅サイズ：1024px
> 最大ファイルサイズ：1MB

出来上がり
<img src="https://qiita-image-store.s3.amazonaws.com/0/221423/82ba44f6-8ad5-53a7-89aa-ba8cce1b7c46.jpeg" width=50%><img src="https://qiita-image-store.s3.amazonaws.com/0/221423/70c5ddf7-c0de-5d19-8dce-c755e2f6081b.jpeg" width=50%>
ショコラロールケーキ:yum:

javascriptってあんまりちゃんと書いたことなかったり:joy:

```javascript:linebot.gs
var CHANNEL_ACCESS_TOKEN = 'YOUR_TOKEN';
var MAX_COLUMN = 10;
var CONVINI_TYPE = ['セブン','ファミマ','ローソン','ミニストップ'];
var FUNCTIONS = {
  'セブン': postSevenProducts,
  'ファミマ': postFamimaProducts,
  'ローソン': postLawsonProducts,
  'ミニストップ': postMinistopProducts
}

function doPost(e) {
  var reply_token= JSON.parse(e.postData.contents).events[0].replyToken;
  if (typeof reply_token === 'undefined') {
    return;
  }
  var user_message = JSON.parse(e.postData.contents).events[0].message.text;
  var cards; 
  if (user_message.indexOf('新商品') != -1){
    CONVINI_TYPE.some(function(value) {
      if (user_message.indexOf(value) != -1){
        var term = user_message.indexOf('来週') != -1 ? '来週' : '今週';
        cards = FUNCTIONS[value](term);
        return true;
      }
    });
  }
  else {
    return;
  }
  
  var payload;
  if (cards === "null") {
    var notfound_text = "まだ発表されてないよ";
    payload = (
      {
        'replyToken': reply_token,
        'messages': [{
          'type': 'text',
          'text': notfound_text,
        }],
      }
    );
  }
  else {
    payload = (
      {
        'replyToken': reply_token,
        'messages': [{       
          "type": "template",
          "altText": user_message + "の検索結果",
          "template": {
            "type": "carousel",
            "columns": cards
          }
        }],
      }
    );
  }

  var url = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify(payload),
  });

  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
```

# ローソン対応

ローソンの新商品ページは週毎に一意なURLが割り当てられているようなのですが、その規則がわかりません。
呆然とページを行ったり来たりしていたところ、最新のページはhttp://www.lawson.co.jp/recommend/new/
からリダイレクトされていることに気づきました。
すでに最新ページが今週分であれば来週分は未発表扱いになるのですが、新商品ページは金曜に更新されているみたい？なので金曜～日曜までは1週間分前の商品ページまでたどり、今週分としています。

```javascript:lawson.gs
// ----------------------------------------------------------------------------
// Post Lawson Products
//
// ローソン新商品取得
// http://www.lawson.co.jp/recommend/new/
// ----------------------------------------------------------------------------

var LAWSON_TERMS = {
  '発表日以前今週': 'thispage',
  '発表日以降今週': 'prvpage',
  '発表日以降来週': 'thispage'
};
var LAWSON_PUB_DAY = 5;
var LAWSON_BASE_URL = 'http://www.lawson.co.jp';
var LAWSON_GOODS_DIR = '/recommend/new/';

function postLawsonProducts(term) {
  var dt = new Date();
  if (0 < dt.getDay() && dt.getDay() < LAWSON_PUB_DAY) {
    term = '発表日以前' + term;
  }
  else {
    term = '発表日以降' + term;
  }

  if (term == '発表日以前来週') {
    return "null";
  }
  
  var url = lawsonURL(LAWSON_TERMS[term]);
  return makeLawsonAttachments(url);
}

function lawsonURL(page) {
  var url = LAWSON_BASE_URL + LAWSON_GOODS_DIR;  
  var html = UrlFetchApp.fetch(url).getContentText();
  
  if (page == 'prvpage') {
    url = LAWSON_BASE_URL + html.match(/<li><a href="([^"]+)">[0-9]+\/[0-9]+発売<\/a><\/li>/)[1];
  }
  
  return url
}

function makeLawsonAttachments(url) {
  var cards = [];
  var html = UrlFetchApp.fetch(url).getContentText();
  var items = Parser.data(html).from('<p class="img">').to('</li>').iterate();
  var cardsize = items.length;
  if (cardsize <= 1) return "null";
  if (cardsize >= MAX_COLUMN) cardsize = MAX_COLUMN;
  for (var i = 0; i < cardsize; i++) {
    var link = LAWSON_BASE_URL + items[i].match(/<a href="(.+?)">/)[1];
    var image = items[i].match(/<img src="([^"]+)" width=/)[1];
    var name = items[i].match(/<p class="ttl">(.+?)<\/p>/)[1];
    var price = items[i].match(/<p class="price"><span>(.+?)<\/span><\/p>/)[1].replace('</span><span>', '');
    var launch = items[i].match(/<p class="date">(.+?)<\/span><\/p>/)[1].replace('<span>', '');
    var calory = items[i].match(/<p>(.+?)<\/p>/) ? items[i].match(/<p>(.+?)<\/p>/)[1] : "";

    var card = (
      {
        "thumbnailImageUrl": image,
        "title": name,
        "text": price + "\n" + launch + "\n" + calory,
        "actions": [
          {
            "type": "uri",
            "label": "くわしく！",
            "uri": link
          }
        ]
      }
    );
    cards.push(card);
  }

  return cards;
}
```
まれにカロリーが記載されていない商品があることに注意:writing_hand:

# ミニストップ対応

ミニストップはJSONで書かれた新商品のリストをhtmlに展開しているようです。
Parseが簡単ですね。
今週・来週の切り分けはちゃんと対応しきれていないので、そのうちやろうと思います。

```javascript:ministop.gs
// ----------------------------------------------------------------------------
// Post Ministop Products
//
// ミニストップ新商品取得
// https://www.ministop.co.jp/syohin/js/recommend.json
// ----------------------------------------------------------------------------

var MY_MINISTOP_REGION = ['関東', '東海', '全国'];
var MINISTOP_BASE_URL = 'https://www.ministop.co.jp';
var MINISTOP_PRODUCTS_DIR = '/syohin/js/recommend.json';

function postMinistopProducts(term) {
  if (term === '来週') {
    return "null";
  }
  var url = MINISTOP_BASE_URL + MINISTOP_PRODUCTS_DIR;
  return makeMinistopAttachments(url);
}

function checkMinistopRegion(region) {
  return MY_MINISTOP_REGION.some(function(value){
    return (region === value);
  });
}

function makeMinistopAttachments(url) {
  var cards = [];
  var json = JSON.parse(UrlFetchApp.fetch(url).getContentText());
  for (var i = 0, cardsize = 0; (i < json.length) && (cardsize < MAX_COLUMN); i++) {
    var link = MINISTOP_BASE_URL + json[i]["link"];
    var image = MINISTOP_BASE_URL + json[i]["image"];
    var name = json[i]["title"];
    var price = json[i]["price"];
    var launch = json[i]["release"];
    var region = json[i]["region"];
    if (!checkMinistopRegion(region)) {
      continue;
    }
    cardsize++;

    var card = (
      {
        "thumbnailImageUrl": image,
        "title": name,
        "text": price + "\n" + launch + "\n" + region,
        "actions": [
          {
            "type": "uri",
            "label": "くわしく！",
            "uri": link
          }
        ]
      }
    );
    cards.push(card);
  }
  
  return cards;
}
```

# まとめ

セブンとファミマについては割愛しますが、カルーセルテンプレートの使用に際して、画像のURLがhttpsでなければならないというMessaging APIの制約に少しつまづきました。
とりあえずGoogleのUrlShortenerAPIで短縮URLにしてしのいでいます:sweat:

コンビニの新商品ページってみんな同じだと思っていましたが、実は結構三者三様なんですね。

# これから

[リッチメニュー](https://developers.line.me/ja/docs/messaging-api/using-rich-menus/)とやらを使って、今週分をワンタップで出せるようにしたかったのですが、有料プランのみの対応らしく断念。

う～ん:thinking:どうしましょうか。
あと残ってるコンビニといえば[デイリーヤマザキ](http://www.daily-yamazaki.jp/new/)とか？
スタバの[メニュー](http://www.starbucks.co.jp/beverage/)を季節もの/ラテ/ティー/フロート/フラペチーノとかざっくり指定して表示させるBotも面白いかも:coffee: