LoginSignup
5
4

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-08-26

Message Buttons 楽しそう…

面白そうなのにあまり流行っていないでおなじみ、SlackのMessage Buttons

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

…あれ?

一度認証しているはずなのに、Dyno再起動のたびに再認証が必要になっちゃう…。
元のサンプルにあったコードがないからかな? と思い、追加しましたがやっぱりだめ。

どうやら、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のソースを眺めていたところ、こんなコードを発見。
この辺のプロパティを実装すれば、カスタムストレージが使えるってことですね!
ということでbotはheroku上で動かし、ストレージとしてS3を使うことにしたのでした。

ストレージクラスのソースはこんな感じです。
デフォルトのsimple_storage.jsパク参考にしました。
S3用APIのAccessKeyとSecretKeyは事前に取得しておいてください。

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はこんな感じに修正します。

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しちゃうのも対策しなきゃ…。

5
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4