walkingmask’s development log

IT系の情報などを適当に書いていきます

MENU

Google Apps Scriptのバージョンアップをしないアーキテクチャ

どうも、GAS芸人のwalkingmaskです。

walkingmask.hatenablog.com

walkingmask.hatenablog.com

walkingmask.hatenablog.com

GASいじる度に毎回思うのが「公開するときバージョン上げるのだるいな」です。

そこで今回新しいSlackボットを試作するにあたり、「Googleドライブ上に配置したJavaScriptをGASから実行する」ことによってGASバージョンアップをしなくてもコマンドや定期ジョブの追加ができるアーキテクチャを作って見たのでログっておこうと思います。

GASからGoogleドライブ上に保存してあるJavaScriptを実行する

できるの?と思いますが、簡単にできます。eval使うだけです。

function doJS (JSFileName, args) {
  var scriptRoot = DriveApp.getFileById(properties.get('LOG_ID')).getParents().next(); 
  var JSFile = DriveApp.getFilesByName(JSFileName).next();
  var JSText = JSFile.getBlob().getDataAsString('utf8');
  return eval('(function () {' + JSText + '}());');
}

properties.get('LOG_ID'))はGASと同階層に置いてあるログ用のドキュメントのIDをスクリプトプロパティから取得しているだけで、DriveAppでそのファイルを開いて親フォルダを取得、つまりGASの入っているフォルダを取得しています。

そのフォルダ以下にあるJSFileNameのJavaScript(例えばtest.js)を取得しています。ファイル名はフォルダ以下で一意である必要があります。

ここまでは実装次第で色々変わる部分だと思うので前置きです。

メインは次の2行で、JSFileを文字列として読み込み、evalで実行します。雑ですが、即時関数で実行し結果をreturnする事で返り値を取れ、引数はargsで渡せる想定です。

簡易redisもどき

jsを実行できるようになったからといって、これを実行するコードをgs内で定義してはバージョンアップは避けられません。

Slackで叩いたコマンドとjsファイルを紐づけられるようにredisもどきを作ります。jsonをstringfyして使うようにすればもっとリッチな表現ができたり、ガチって作ればDB的にも扱えそう。

var redis = {
  spreadsheet: SpreadsheetApp.openById(properties.get('REDIS_ID')),
  db: {},
  selectedDBName: null,
  select: function (DBName) {
    if (! this.db[DBName]) {
      var _db = {};
      this.spreadsheet.getSheetByName(DBName).getDataRange().getValues().map(function(x) { _db[x[0]] = x[1]; });
      this.db[DBName] = _db;
    }
    this.selectedDBName = DBName;
    return this;
  },
  get: function (key) {
    return this.db[this.selectedDBName][key];
  }
}

これで、REDIS_IDを持つスプレッドシートでkey-valueを保存できます。このスプレッドシートはバージョンアップ不要で書き換えられます。

スプレッドシートcommandsというシートを用意し、test,slack-bot-command-test.jsのように保存し、ドライブにjs/slack-bot-command-test.jsを配置します。

doJS(redis.select('commands').get('test'))といった感じで実行できます。

コマンドとして実行

まだ'test'スクリプトに含まれてしまっていますね。

function doPost (e) {
  if (e.parameter.token != redis.select('constants').get('SLACK_TOKEN')) return;
  var commandArgs = e.parameter.text.split(' ').slice(1);
  if (commandArgs.length < 1) return;
  var subcommand = commandArgs[0];
  var args = {
    commandArgs: commandArgs.slice(1),
    parameter: e.parameter
  };
  return doJS(redis.select('commands').get(subcommand), args);
}

これでバージョンアップなしにコマンドをサクサク作成できるアーキテクチャができました!

雑ですが、いい感じな気がします。

ジョブとして実行

function doJob (period) {
  var jobs = redis.select('schedules').get(period);
  if (jobs.length == 0) return;
  jobs = jobs.split(',');
  for (var i = 0; i < jobs.length; i++) {
    var job = redis.select('jobs').get(jobs[i]);
    doJS(job, null);
  }
}

function doMinutely () {
  doJob('minutely');
}

function doHourly () {
  doJob('hourly');
}

function doDaily () {
  doJob('daily');
}

redisスプレッドシートにschedulesというシートを用意して、minutely,testという記述をします。

そしてjobsというシートにtest,slack-bot-job-test.jsと記載し、slack-bot-job-test.jsをドライブに配置。

あとはdoMinutely, doHourly, doDailyをトリガーに設定すると簡易crontabみたいな感じになります。

まとめ

eval中でも実行はgs内なのでGoogle APIが使えます。なので実行自体は普通のgs感覚です。

引数戻り値は後々工夫が必要になるかもしれませんね。

まだまだ試作段階ですが、これでサクサクコマンド作って行けたら良いですね。

とはいえdoPostの中は割と柔軟に変えていけたらと思うので、バージョンアップは完全には排除できてないです。

doPostの中身をdoJS('slack-bot-do-post.js')のようにできれば...?