Google Apps Scriptで作るslackのスラッシュコマンド 2018年6月版

# Google Apps Scriptで作るslackのスラッシュコマンド

slackでbotを作る方法はいくつかありますが、スラッシュコマンドは、導入者が入っていない窓でも利用可能であるなどの利点があります。
Google Apps Script(GAS)で作ってみたらこれが非常に具合が良かったので、作り方を説明します。

## つくったもの

https://github.com/skuroki/karimono

会社でよく「あれって今、誰が借りてるの？」ってなっている様子が見られるので、これを管理するためのスラッシュコマンドです。

## 前提：slackのスラッシュコマンドの仕様

https://api.slack.com/slash-commands

* 誰かがスラッシュコマンドを実行すると、事前に設定したURLにPOSTリクエストが送られてくる
    * どのワークスペースの、どのチャンネルで、誰が、どういう引数で実行したか、などの必要な情報がパラメータに入っている
* リクエストに対して、テキストまたはJSONでレスポンスを返すと、その内容がスラッシュコマンドの結果として表示される
* レスポンスを3秒以内に返す必要があり、返せなかったらスラッシュコマンドの結果はエラーの表示になり、遅れて返したレスポンスの内容は表示されない
    * ただし、リクエストパラメータに送られてくる専用のURLに30分間メッセージを送ることができるので、時間がかかる処理は一旦「わかった」と返して、あとから結果を通知するという方法が取れる（今回は使用していない）

## Google Apps Scriptでウェブアプリを公開できる

GASは所定のルールに従った関数を定義しておけばウェブアプリとして公開することができます。

[2013年度版 5分で始めるWebアプリケーション(Google Apps Script)](https://qiita.com/soundTricker/items/a4878d7e3100082576e4)

5年前の記事ですが、現在でも概ねこの通りに行うことで公開できました。

スラッシュコマンドのリクエストを受けつけるために、「アプリケーションにアクセスできるユーザー」を「全員（匿名ユーザーを含む）」に設定する必要があります。

スラッシュコマンドのリクエストはPOSTメソッドなので、doGetではなくdoPost関数を用意する必要があります。

先述の仕様にあわせたレスポンスができる必要がありますが、まずは応答する！というところまで行くのが良いと思うので、それについては後述します。

## slackのスラッシュコマンドを登録する

「slack appの作成」「slack appのワークスペースへの登録」と段階を踏む必要があります。

### slack appの作成

https://api.slack.com/slack-apps

ここが自分でslack appを作る人向けの説明ページなのですが、「Create a slack app」から作ることができます。
（自分で設定しない限りは全世界に公開はされません）

### スラッシュコマンドの設定

![image.png](https://qiita-image-store.s3.amazonaws.com/0/17353/8da8313a-515a-505f-1f75-24a242bb4b47.png)


作ったら、「Add features and functionality」の「Slash Commands」から設定します。
「Create new command」で新規登録画面になるので、コマンド名と、GASで公開したウェブアプリの「現在のウェブ アプリケーションの URL」を入力します。

### ワークスペースへの登録

appを作っただけでは使えるようにならないので、登録します。
「Install your app to your workspace」から行います。

### ここまでできたら

自分のワークスペースでスラッシュコマンドを打つと、応答が（何も変えてなければ生のHTMLになりますが）帰ってくるはずです。

## 実装の詳細

https://github.com/skuroki/karimono/blob/master/index.js

特に難解な実装などはしていないので、一通り読んでもらえばどういうコードを書く必要があるのかはある程度掴んでもらえると思います。
以下ピックアップ。

### JSONで返す

「JSONで返す」ためには、「JSON文字列を作ってレスポンスとして送る」「レスポンスのcontent-typeにJSONを指定する」必要があります。
これらはGASでは以下のように書くことで実現できます。特に何かのrequireなどは必要ありません。

```js
  return ContentService.createTextOutput(JSON.stringify({
    response_type: 'in_channel',
    text: 'hello' })).setMimeType(ContentService.MimeType.JSON);
```

### GASのスクリプトプロパティを使う

https://developers.google.com/apps-script/reference/properties/properties

GASはスクリプト単位でデータを永続化する簡易な仕組みを持っています（他に、ユーザー単位、扱うgoogleドキュメント単位のものがあります）
扱えるのが文字列だけであるKey-Valueストアという感じです。
文字列だけなので、出し入れする時はJSONに変換しています。

```js
var scriptProperties = PropertiesService.getScriptProperties();
var property = scriptProperties.getProperty(name);
var item = JSON.parse(property);
item.borrower = 'alice';
scriptProperties.setProperty(name, JSON.stringify(item));
```

1件づつでなくまとめて取得したり設定することもできます。

### ここまでできたら

スラッシュコマンドでデータを読み書きして好きなレスポンスを返せるようになっているはずです。あとは想像力次第でいろいろできますね。

## 手元に開発環境を作る

簡単な実装ならブラウザ上のスクリプトエディタで書いてしまって良いと思います。
でも段々複雑になってくると、

- 自分の得意なエディタで書けない
- ES6以降の書き方で書けない（アロー関数とか）
- Githubに上げられない
- lintをかけられない

などの事が気になってくるので、手元に開発環境を作ります。

わかる人は

```sh:deploy.sh
#!/bin/bash -ex
npx eslint index.js
npx babel index.js > deploy/index.js
cd deploy
clasp push
clasp version
clasp redeploy `clasp deployments | grep web | head -n1 | awk '{ print $2 }'` `clasp versions | tail -n1 | awk '{ print $1 }'` web
```

これを読めばだいたいわかると思います。説明していきます。
npmの説明はしません。以降で使っているものはだいたいnpmでインストールしています。npm同梱のnpxを使えば特にパスを通さなくてもコマンド実行できます（要はbundle exec）。

### claspでコードのアップロード・ダウンロード・デプロイなどを行う

https://github.com/google/clasp

[Google Apps Scriptの新しい3つの機能 その③ CLI Tool Clasp](https://qiita.com/soundTricker/items/354a993e354016945e44)

GAS公式のコマンドラインツールです。
導入する最初は、 `clasp clone` で作ったコードを取ってきて、以降は手元でのみ作業して `clasp push` でアップロードするのが良いと思います。コードの共有が必要ならgithubなどでやって、GAS本体はデプロイ先と考えたい感じです。
上記のスクリプトでは、

- コードをアップロードする
- アップロードしたコードでバージョン登録する
- 登録したバージョンで、もともと公開しているウェブアプリを更新（リデプロイ）する

ということをしています。
後述の翻訳を行ってデプロイという形にしたかったので、claspのcloneをしてくるディレクトリは一段掘ってあります。

追記: 2018/06/29時点のバージョンでは、上記デプロイスクリプトだとバージョン50までしかデプロイできませんでした。これは1.4.1で解消されるようですが、未リリースなのと、表示順が変わるためスクリプトの修正が必要になりそうです。

### ES6以降の書き方で書く

今回はindex.jsだけで完結で良さそうだったので、babel-cliのみ導入しています。
モダンなJSで書く→GASで通る昔のJSにbabelで翻訳してデプロイ用ディレクトリに置く、という流れです。

eslintの設定は以下のような感じです。

```js:.eslintrc.js
module.exports = {
  "extends": ["eslint:recommended", "google"],
  "parserOptions": {
    "ecmaVersion": 8,
  },
  "plugins": [
    "googleappsscript"
  ],
  "env": {
    "node": true,
    "googleappsscript/googleappsscript": true
  },
  "rules": {
    "require-jsdoc": "off",
    "no-console": "off",
    "no-unused-vars": ["error", { "varsIgnorePattern": "doPost" }],
    "object-curly-spacing": ["error", "always"],
    "arrow-parens": "off"
  }
};
```

多少好みが入っていますが、GASの開発にそのまま使えると思います。
やっていることはざっくり

- 定番の規約がベース
- ES2017でパースする
- GASで使えるクラスなどの設定を追加して「そんなものないよ」と怒られなくする
- 「doPostが呼ばれてないよ」と怒られなくする
- console.logを使っても怒られないようにする
    - Stackdriverにログが飛んで、見れるようになります
- あとはだいたい好み

という感じです。

### ここまでできたら

さくさく書けるようになったし、JSONで返すようなちょっとしたAPIサーバーを立てるの、もうだいたいGASでいいんじゃね？って気持ちになってきませんか？私はなりました。サーバー代も（小規模アクセスなら）ほとんどかからないし最高では。

## 余談：Firebaseで挫折した話

最初はこのスラッシュコマンドはcloud functions for firebaseでfirestoreを使って開発していたのですが、安定して3秒以内にレスポンスを返す方法が見つからず、断念して途中でGASに乗り換えています。
安定して3秒以内にレスポンスを返す事ができた人は是非記事でも書いて教えてほしい感じです。

## 謝辞

いくつかQiitaの記事を紹介させてもらっていますが、今回の実装にあたって、先人の記事で多くの情報を得ることができました。著者のみなさん、ありがとうございます。