読者です 読者をやめる 読者になる 読者になる

AWS Lambda を利用して Nexus 6 を (σ´∀`)σゲッツ!!

Nexus 6 が欲しいのになかなか買えない!!!!!!
SIM カードが不要なので Google Play で買おうと思ってるのですが、いつ見ても

現在在庫切れです。しばらくしてからもう一度ご確認ください。

色や容量にはこだわらないのに、年末から折を見て確認しているのに、いつ見ても

現在在庫切れです。しばらくしてからもう一度ご確認ください。

(#`Д´)ノノ┻┻;:'、・゙


こうなってしまうと本当に在庫が復活するのか疑わしい物です。
どれ、ちょっと自動で在庫をチェックして、在庫が復活した際に通知を送りましょう。

Azure に tie-in してよいのであれば、すぐに作れます。
なぜなら Azure には Node.js をバックエンドに GUI からスケジューラを作成できるお手軽便利機能があるからです。

今回はそれではつまらないのと(Azure が優秀過ぎる)、Amazon SNS のトピックが便利(モバイルデバイス以外にもメールにも通知を行いたい)なため最近ホットな AWS Lambda を使ってみます。
ところが、AWS Lambda にはスケジューラ機能がありません。そのうち実装されると思いますが、とにかく今はないので、代替方法を探します。

皆さん、S3 を利用したり Lambda API で自分自身を呼び出したりとなんとか頑張ってループしているようです。
今回の例ですと、後者の Lambda API で 45 秒間隔で Nexus 6 のサイトをチェックし、在庫がある場合に通知を行う Lambda を作ってみましょう。
また、通知を連続で送ってしまわないよう、通知済みを記録するファイルを作成します。このファイルがある場合には、通知済みとして通知を行わず、ファイルが存在しない場合には通知を行うようにします。

var AWS = require('aws-sdk');
var HTTPS = require('https'); // http な場合は var HTTP = require('http'); とします

exports.handler = function(event, context) {
  console.log('Starting nexus 6 stock checker...');
  console.log(event);

  var lambda = new AWS.Lambda({
    accessKeyId: 'XXXXXXXXXXXXXXXXXXX', // FIXME: AWS Lambda の IAM アクセスキー
    secretAccessKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', // FIXME: AWS Lambda の IAM シークレットキー
    region: 'us-east-1' // FIXME: Lambda のリージョン
  });

  setTimeout(function() {
    var params = {
      FunctionName: 'checkNuxus6Stock', // Lambda 関数名
      InvokeArgs: '{}'
    };

    lambda.invokeAsync(params, function(err, data) {
      if (err) {
        console.log('Unexpected error encountered while invoking lambda function.');
        console.log(err, err.stack);

        return;
      } else {
        console.log(data);
      }

      try {
        // get なのでキャッシュされないよう URL に _ パラメータ(名前は何でもいい、値が一意であれば)をくっつけておきます
        check('https://play.google.com/store/devices/details/Nexus_6_32_GB_%E3%83%80%E3%83%BC%E3%82%AF%E3%83%96%E3%83%AB%E3%83%BC?id=nexus_6_blue_32gb&_=' + new Date().getTime(), context); // 青/32GB
        check('https://play.google.com/store/devices/details/Nexus_6_64_GB_%E3%83%80%E3%83%BC%E3%82%AF%E3%83%96%E3%83%AB%E3%83%BC?id=nexus_6_blue_64gb&_=' + new Date().getTime(), context); // 青/64GB
        check('https://play.google.com/store/devices/details/Nexus_6_32_GB_%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89_%E3%83%9B%E3%83%AF%E3%82%A4%E3%83%88?id=nexus_6_white_32gb&_=' + new Date().getTime(), context); // 白/32GB
        check('https://play.google.com/store/devices/details/Nexus_6_64_GB_%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89_%E3%83%9B%E3%83%AF%E3%82%A4%E3%83%88?id=nexus_6_white_64gb&_=' + new Date().getTime(), context); // 白/64GB

        context.done();
      } catch (e) {
        console.log(e);
      }
    });
  }, 45000); // 45 秒後に自身を実行


  function check(url, context) {
    HTTPS.get(url, function (res) { // http な場合は HTTP.get とします
      console.log(url + ' response:' + res.statusCode);

      if (res.statusCode < 200 || res.statusCode >= 300) {
        return;
      }

      var body = '';

      res.on('data', function(chunk) { // レスポンスをドラゴンボールのようにかき集めます
          body += chunk;
      });

      res.on('end', function() { // レスポンスから在庫がないときに存在する HTML コードの検索を行い、在庫状況をチェックします
        console.log(body);

        if (body != null && body.indexOf('We are out of inventory. Please check back soon.') == -1) {
          console.log('Notifying me...');
          notifyMe();
        }
      });
    }).on('error', function(e){
      console.log('error', e);
    });
  }

  function notifyMe() {
    var s3 = new AWS.S3({ apiVersion: '2006-03-01' });

    s3.getObject({ Bucket: 'XXXXXXXXXX', Key: 'nexus6_has_notified'}, function(err, data) { // FIXME: 通知状況を確認するためのバケットとキー(パス)
      if (err) { // ファイルが存在しない場合には未通知とみなし通知を行います
        console.log('Object not found.');

        var sns = new AWS.SNS({
          apiVersion: '2010-03-31',
          region: 'us-east-1' // FIXME: AWS SNS のリージョン
        });
        var payload = {
          aps: {
            alert: 'Nexus 6 is now available!',
            sound: 'default',
            customProperty: 'We are Nexsus!' // デバイスに独自にデータを渡したい場合この辺に突っ込んどきます(iPhone の場合)
          }
        };
        var message = {
          default: 'Nexus 6 is now available!',
          APNS: JSON.stringify(payload) // iPhone の場合(GCM, Baidu 等を用いて通知したい場合それぞれのプラットフォームに応じた形式の JSON を格納します)
        };
        var params = {
          TopicArn: 'arn:aws:sns:us-east-1:XXXXXXXXXXX:XXXXXXX', // FIXME: AWS SNS の通知先トピックARN
          MessageStructure: 'json',
          Message: JSON.stringify(message)
        };

        sns.publish(params, function(err, data) {
          if (err) {
            console.log('Unexpected error encountered while Notifying me.');
            console.log(err);
          }
        });

        s3.putObject({ // 通知済みであることを S3 に保存します
          Bucket: 'XXXXXXXXXX', // FIXME: 通知状況を保存するバケット
          Key: 'nexus6_has_notified',
          Body: new Buffer('1', 'binary'), // 今回の例だとファイルが「存在する/しない」のみの制御なので何が書いてあっても問題なし
          ContentType: 'application/octet-stream'
        }, function(err, res) {
          if (err) {
            console.log('Unexpected error encountered while putting object.');
            console.error(err);
          }
        });
      } else {
        console.log('Object found.');
      }
    });
  }
};

Lambda 作成時の名前を「checkNuxus6Stock」、タイムアウトを「60」秒にして作成すれば OK です。