Amazonの商品を注文するLINEbotを作ろうとしてうまく行かなかった話

# はじめに

## 背景・解決したかった課題
いつも、シャンプーを始めとした消耗品を購入するときは、Amazonにお世話になっています。ただ、最近は、シャンプーが残り少なくなってきたときに、「あとで注文しておこう」と思うものの、数分後には、忘れてしまう事象が頻発していました。

まず思いつく解決策としては、Amazon Dash Buttonの導入ですが、なにか消耗品が切れるたびに商品を注文していると、注文した数だけ荷物の受け取りの手間が発生してしまいます。できれば、日々の生活の中で「消耗品が少なくなったなー」と思ったときには、商品の購入ではなく「カートに入れる」にとどめておいて、週末などに、今週1週間でカートに入った商品を一括注文するというソリューションを構築したいと思いました。

## 取り組みたかったこと
1. LINEbotに「いつものシャンプーを購入」などのメッセージを送る
2. 成功すると、Amazonのカートに商品が追加され、「カートに追加しました」というメッセージを返す
3. 週次でカートの中の商品を一括購入する(これは手動でもOK)

## 結論としてどうだったか
- Amazonはコンシューマー向けにAPIを公開しておらず、カート追加は難しい
- WebオートメーションでAmazonにログインを試みるも、CAPTCHAにより弾かれる

結論からいうと失敗でした。

# 取り組んだこと
## いつもの消耗品リストをGoogleSpreadSheetに登録する
アーキテクチャは、可能な限り無料で構築したいものです。そのため、まずはGoogleSpreadSheetにいつもの商品の品名とURLを登録しておき、それらのデータをGoogleAppsScript(GAS)を使って操ることを考えました。

まずは、以下の「いつもの消耗品リスト」を作成します。
![スクリーンショット 2018-12-16 2.15.19.png](https://qiita-image-store.s3.amazonaws.com/0/280372/f1dc0c1d-0528-3051-1589-e26452bcb8e2.png)

品名から、URLを返すようなGASを作成します。

```javascript
function findAmazonUrl(itemName){
  var sheet = SpreadsheetApp.getActiveSheet();
  var data = sheet.getDataRange().getValues();
  
  for (var i = 1; i < data.length; i++) { //i=0(スプレの1行目)はタイトル行のため検索しない
    if(data[i][1] === itemName){
      return data[i][2];
    }
  }
  return false;
}
```


## LINEBotを作成する

LINE Developers( https://developers.line.biz/ja/ )のサイトから、Botを作成し、ACCESS_TOKENの取得や、Webhookの設定などを行います。具体的な設定方法については、ここでは割愛します。
まずは、LINEのメッセージで「シャンプー」と送ると、「いつもの消耗品リスト」を検索し、そのURLを返してくれるようなBotを目指します。

先程の`findAmazonUrl`を利用して、以下のようなスクリプトを書きました。

```javascript

var ACCESS_TOKEN = (YOUR TOKEN);

function doPost(e) {
  var event = JSON.parse(e.postData.contents).events[0];
  var replyToken = event.replyToken;
  var userMessage = event.message.text;
  
  var url = 'https://api.line.me/v2/bot/message/reply';
  
  var itemName = userMessage;
  var amazonURL = findAmazonUrl(itemName);
  var replyMessage = amazonURL || '該当なし';

  UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': replyToken,
      'messages': [{
        'type': 'text',
        'text': replyMessage,
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
```

実際にLINEでテストしてみると...

<img width="375" alt="写真 2018-12-16 2 22 33.png" src="https://qiita-image-store.s3.amazonaws.com/0/280372/05d24924-f95d-8f52-5d9a-fd71dbccf936.png">


ここまでは順調！！

## AmazonのAPIなどを叩いてカートに商品を追加したかった...

さて、あとはGASにAmazonのAPIを叩くコードを書いて...と思っていたのですが、どうやら通販のAmazonのほうは、コンシューマー向けのAPIの用意が無いことに気づきました。ついついAWSのCLIのノリで進めてしまっていた...

せっかくBotも作ったのに、このまま諦めるのももったいないので、もう少し足掻いてみることにしました。そうだ、サーバレスでWebオートメーションを動かそう！！！

##Google Cloud FunctionsでPuppeteerを動かし、Amazonにログインする

Puppeteerは、Googleが開発する、Chromeブラウザをヘッドレスで操作することができるNode.jsのライブラリです。これを使うと、人間が手動でやっている「ログインする」「カートに追加するボタンを押す」などの操作を、自動で行うことができるはずです！

アーキテクチャとしては、相性が良さそうなGCPの「Cloud Functions」を利用します。AWSでいうLambdaのようなポジションの機能です。

Google公式から以下のような記事も公開されており、利用も容易にできそうです。

Introducing headless Chrome support in Cloud Functions and App Engine
https://cloud.google.com/blog/products/gcp/introducing-headless-chrome-support-in-cloud-functions-and-app-engine

###トリガーをHTTPとした関数を作成する

早速、Cloud Functionsから新しい関数を作成します。トリガーはHTTPを、ランタイムはNode.js 8を選択し、ソースコードは以下のようなものをデプロイします。
なお、Amazonにログインするためのメールアドレスやパスワードは、環境変数として登録しておきます。

```javascript:index.js
const puppeteer = require('puppeteer');
let page;

async function getBrowserPage() {
  const browser = await puppeteer.launch({args: ['--no-sandbox']});
  return browser.newPage();
}

exports.addCart = async (req, res) => {
  const itemUrl = req.query.url;
  
  if (!page) {
    page = await getBrowserPage();
  }
  
  const options = {
    viewport: {
      width: 1024,
      height: 820,
    },
    userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
  };

  await page.emulate(options);
  let loadWait = page.waitForNavigation({waitUntil: "domcontentloaded"});
  
  // Amazonにアクセス
  await page.goto('https://www.amazon.co.jp');
  await loadWait;
  
  // ログインページに遷移
  await page.click('a#nav-link-accountList');
  await loadWait;
  console.log(await page.content());
  
  // ログイン
  await page.type('#ap_email', process.env.EMAIL);
  await page.type('#ap_password', process.env.PASSWORD);
  await page.click('#signInSubmit');
  await loadWait;

  // 商品ページに遷移
  await page.goto(itemUrl);
  await loadWait;
  
  // カートに追加！
  await page.click('#add-to-cart-button');
  await loadWait;

  // スクリーンショットを撮影し、レスポンスとして返す
  const imageBuffer = await page.screenshot({fullPage: true});
  res.set('Content-Type', 'image/png');
  res.send(imageBuffer);
};
```

```json:package.json
{
  "name": "add_cart",
  "version": "0.0.1",
  "dependencies": {
    "puppeteer": "^1.6.2"
  }
}
```

### テストをしてみるも失敗におわる

さて、ここまでできたらいよいよ「カートに入れる」のテストをしてみます。実際にブラウザのURLに、以下のような文字列をぶち込み、GETでアクセスしてみます。

```
https://us-central1-project-id-｛your_project_id｝.cloudfunctions.net/addCart?url=https%3A%2F%2Fwww.amazon.co.jp%2Fgp%2Fproduct%2FB00XTYCTK8
```

...なぜかうまくいかない。

デバック用に、スクリーンショットを作成するスクリプトを挟んで、返ってきた結果がこちら。

![addCart.png](https://qiita-image-store.s3.amazonaws.com/0/280372/73a12cbe-d2b6-5c59-5e91-15382550cf75.png)

これはもう諦めですね。


#振り返り
やはり通販サイトなので、不正ログイン対策などはきっちりされていました。個人的にはコンシューマー向けAPIくらいは公開してほしい！と思っているのですが、やはり難しいのでしょうか？(私が探し足りなかったのかもしれません。ご存知の方がいらっしゃれば、教えていただきたいです！)

今回は、完全に個人の遊びのプロジェクトだったわけですが、これが会社の数ヶ月かかるプロジェクトの終盤だと思うと震えが止まりませんね。事前の利用する技術や実現可能性の調査は大切です。また、小さく作って、早期に問題を発見することの大切さも学びました(本当は、LINE経由で「いつもの消耗品リスト」に消耗品を追加できたり、LINEからGASに送られてくるリクエストの署名を検証したり、やりたいことはいろいろあったのですが、まずはMVPを意識したからこそ、早期に問題に気づけました)。

いまはAlexaの購入を検討しています。

#参考文献

以下の情報を非常に参考にさせていただきました。ありがとうございます！

Google Apps ScriptでLINE BOTつくったら30分で動かせた件
https://qiita.com/hakshu/items/55c2584cf82718f47464

Cloud Functions with Puppeteer + Google Apps Script でスクレイピングサーバーをサクッと作る
https://qiita.com/howdy39/items/2f355fea8340a35aa5da