heroku上のBotkitのストレージをS3にする

## Message Buttons 楽しそう…
面白そうなのにあまり流行っていないでおなじみ、Slackの[Message Buttons](https://api.slack.com/docs/message-buttons)。

[slack新機能！message buttonを使ってbotkitをレベルアップさせる！](http://qiita.com/LOUIS_rui/items/3692ddeab6c7773abd11)
にならって、herokuのFreeプランで構築してみました。
基本的には上記のリンクの手順で進めれば、ボタンの表示→お返事まではすぐだと思います。
僕は途中でSlackAppへの各種URL登録を飛ばして若干はまりました…。

## …あれ?
一度認証しているはずなのに、Dyno再起動のたびに再認証が必要になっちゃう…。
[元のサンプルにあったコード](https://github.com/howdyai/botkit/blob/master/examples/slackbutton_bot_interactivemsg.js#L313-L332)がないからかな? と思い、追加しましたがやっぱりだめ。

どうやら、Botkitはローカルの`db_slackbutton_bot`なるフォルダに、OAuthのトークンを保存していて、herokuのFreeプランだとこれが消えちゃうんですね。

### (追記)
お世話になった記事の@LOUIS_ruiさんにコメントしていただきました。
HerokuのmLabというプラグインを使えば簡単に永続化できるようです。
この記事の存在意義とは。

## もういい、EC2でたてよう
ここでhttpsサーバ必須が立ちはだかります
nginxたててhttpsをローカルのNodeへ流してみたのですが、オレオレ証明書だとだめ。(Appページの Interactive Messages でURLを登録できません)
証明書をlet's encryptで取得しようとしたのですが、デフォルトのドメイン(xxxxx.ap-northeast-1.compute.amazonaws.com的なやつ)だと作成に失敗しました。

## S3をストレージにしよう!
呆然とBotkitのソースを眺めていたところ、こんな[コード](https://github.com/howdyai/botkit/blob/master/lib/CoreBot.js#L950-L964)を発見。
この辺のプロパティを実装すれば、カスタムストレージが使えるってことですね!
ということでbotはheroku上で動かし、ストレージとしてS3を使うことにしたのでした。

ストレージクラスのソースはこんな感じです。
デフォルトの[simple_storage.js](https://github.com/howdyai/botkit/blob/master/lib/storage/simple_storage.js)を~~パク~~参考にしました。
S3用APIのAccessKeyとSecretKeyは事前に取得しておいてください。

```js:s3_storage.js
/*
Storage module for bots.
Using AWS S3 storage.

Configuration:
  accessKey: s3 access key
  secretKey: s3 secret key
  bucket: target bucket
  path: path to s3 folder
  region(optional): s3 region (default: us-east-1)
*/

var aws = require('aws-sdk');
var async = require('async');

module.exports = function (config) {

  if (!config) {
    return {};
  }

  var teams_db = config.path + '/teams/';
  var users_db = config.path + '/users/';
  var channels_db = config.path + '/channels/';
  var bucket = config.bucket;

  aws.config.update({
    accessKeyId: config.accessKey,
    secretAccessKey: config.secretKey,
    region: config.region || 'us-east-1'
  });

  var objectsToList = function (cb) {
    return function (err, data) {
      if (err) {
        cb(err, data);
      } else {
        cb(err, Object.keys(data).map(function (key) {
          return data[key];
        }));
      }
    };
  };

  var s3 = new aws.S3();
  var put = function (id, data, cb) {
    var param = {
      Bucket: bucket,
      Key: id,
      Body: JSON.stringify(data)
    };
    // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#putObject-property
    s3.putObject(param, function (err, data) {
      if (err) {
        return cb ? cb(err) : err;
      }
      return cb ? cb(null, data) : data;
    });
  };
  var get = function (id, cb) {
    var param = {
      Bucket: bucket,
      Key: id
    };
    // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getObject-property
    s3.getObject(param, function (err, data) {
      if (err) {
        return cb ? cb(err) : err;
      }
      var obj = JSON.parse(data.Body.toString());
      return cb ? cb(null, obj) : obj;
    });
  };
  var list = function (prefix, cb) {
    var param = {
      Bucket: bucket,
      Prefix: prefix
    };
    // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#listObjects-property
    s3.listObjects(param, function (err, data) {
      if (err) {
        return cb ? cb(err) : err;
      }

      var all = {};
      var funcs = [];
      data.Contents.forEach(function (content) {
        funcs.push((function (_key) {
          return function (callback) {
            get(_key, function (e, obj) {
              if (!e) {
                all[obj.id] = obj;
              }
              callback(e);
            });
          };
        })(content.Key));
      });

      return async.parallel(funcs, function (error) {
        return cb ? cb(error, all) : all;
      });
    });
  };

  // teams, users, channels と
  // それぞれに get, save, all があればStorageとして使える
  var storage = {
    teams: {
      get: function (team_id, cb) {
        get(teams_db + team_id, cb);
      },
      save: function (team_data, cb) {
        put(teams_db + team_data.id, team_data, cb);
      },
      all: function (cb) {
        list(teams_db, objectsToList(cb));
      }
    },
    users: {
      get: function (user_id, cb) {
        get(users_db + user_id, cb);
      },
      save: function (user, cb) {
        put(users_db + user.id, user, cb);
      },
      all: function (cb) {
        list(users_db, objectsToList(cb));
      }
    },
    channels: {
      get: function (channel_id, cb) {
        get(channels_db + channel_id, cb);
      },
      save: function (channel, cb) {
        put(channels_db + channel.id, channel, cb);
      },
      all: function (cb) {
        list(channels_db, objectsToList(cb));
      }
    }
  };

  return storage;
};
```

エントリポイントのjsはこんな感じに修正します。

```js:index.js
var s3Storage = require('./s3_storage');
...

var controller = Botkit.slackbot({conversations
  // json_file_store: './db_slackbutton_bot/',
  storage: new s3Storage({
    path: process.env.s3Path,
    bucket: process.env.s3Bucket,
    accessKey: process.env.s3AccessKey,
    secretKey: process.env.s3SecretKey
  })
}).configureSlackApp({
  clientId: process.env.clientId,
  clientSecret: process.env.clientSecret,
  scopes: ['bot'],
});

...
```

これで、Dyno再起動時も認証情報をもとにbotが再接続してくれます。
めでたしめでたし。皆さんも Message Buttons 試しましょう!

あ、30分でSleepしちゃうのも対策しなきゃ…。