Google Apps ScriptでSlackの新規(ユーザー|チャンネル|絵文字)を通知するBOTを作った

社内のコミュニケーションツールとして、Slackを使用している企業も多いかと思います。
多くの人が参加するワークスペースでは、日々新たなユーザーが参加し、新たなチャンネルが生まれ、新たな絵文字が追加されていきます。
重要な情報を見逃さないためにも、それらのイベントを特定のチャンネルに通知するBOTをGoogle Apps Scriptで作って運用しています。

## なぜGoogle Apps Scriptなのか
元々は、ノンコーディングで実現するために[Zapier](https://zapier.com/)を使っていました。
しかしながら無料プランだとなにかと制約が厳しく、代替案の検討を余儀なくされました。
当時はまだ社内でAWSが使えなかったこともあり、ひとまずGoogle Apps Scriptで実装することにしたのです。

## システム構成図
あえて図にするまでもありませんが、BOTのシステム構成図は以下の通りです。

![[Slack] <--> [Google Apps Script]](https://qiita-image-store.s3.amazonaws.com/0/132608/cbd8ef8a-1231-ce4e-1b43-577e31db699e.png)

Slackで発生したイベントをGoogle Apps Scriptに投げ、Google Apps Script側でメッセージを生成してSlackに投稿するといった流れです。
IFTTTで言うところの、トリガーとアクションの両方にSlackを設定している状態ですね。

## 仕組み
それでは、実際のソースコードを交えつつBOTの仕組みを見ていきましょう。
ここでは新規チャンネル通知BOTを例にあげますが、他のBOTも概ね同じ仕組みです。

### Slackで発生したイベントをGoogle Apps Scriptに投げる
Slackで発生したイベントを外部に投げるには、[Events API](https://api.slack.com/events-api)を使用します。
Events APIでは、「ユーザーが参加した」「チャンネルが作成された」「絵文字が更新された」など、任意のイベントの発生時に指定したURLにHTTPリクエストを投げてくれます。
新規チャンネル通知BOTでは[channel_created](https://api.slack.com/events/channel_created)というイベントを購読しているため、チャンネルが作成されたときに以下のようなリクエストが投げられます。

```json
{
    "type": "channel_created",
    "channel": {
        "id": "C024BE91L",
        "name": "fun",
        "created": 1360782804,
        "creator": "U024BE7LH"
    }
}
```

余談ですが、SlackはチャンネルIDが`C`で始まりユーザーIDが`U`で始まるなど、ID自体でそれが何を表しているのかが分かるようになっていて良いですね。

### Google Apps Scriptでリクエストを受け取る
Google Apps Scriptでは`doPost`関数でPOSTリクエストを受け取ることができるので、その中に処理を書いていきます。

```js
function doPost (e) {
  var request = normalizeRequest_(e)
  var requestType = request.type

  if (requestType === REQUEST_TYPE_URL_VERIFICATION) {
    var challenge = request.challenge
    return createTextOutput_(challenge)
  } else if (requestType === REQUEST_TYPE_EVENT_CALLBACK) {
    var event = request.event

    if (event.type === EVENT_TYPE_CHANNEL_CREATED) {
      var message = createMessage_(event)
      postToSlack_(message)
    }
  }
}
```

認証のための仕組みとして、リクエストタイプが`url_verification`のときはリクエストに含まれる`challenge`の値をオウム返しする必要があります。
また、リクエストタイプが`url_verification`のときと`event_callback`のときとではリクエストの取り出し方が異なるようなので、`normalizeRequest_`関数であらかじめ正規化しています。

```js
function normalizeRequest_ (e) {
  var postData = e.postData
  var request = postData.contents || postData.getDataAsString()
  return JSON.parse(request)
}
```

### メッセージを生成する
リクエストに含まれているイベントのデータを元に、メッセージを生成します。

```js
function createMessage_ (event) {
  var channel = event.channel
  channel.created = moment(channel.created * 1000).format(MESSAGE_TEMPLATE_DATE_FORMAT)
  return Mustache.render(MESSAGE_TEMPLATE, channel)
}
```

メッセージの文面を環境変数（スクリプトのプロパティ）で柔軟に設定できるようにするために、以下のライブラリを使用しています。

* [Mustache](https://github.com/munierujp/mustache.gs)：テンプレートを元に文字列を生成
* [Moment](https://momentjs.com/)：日時のフォーマットを指定

たとえば、スクリプトのプロパティで以下のように値を設定したとします。

|プロパティ|値|
|---|---|
|`MESSAGE_TEMPLATE`|`<#{{id}}> has created by <@{{creator}}> on {{created}}.`|
|`MESSAGE_TEMPLATE_DATE_FORMAT`|`YYYY[/]M[/]D H[:]mm[:]ss`|

すると、テンプレート内の`{{`と`}}`で囲まれた変数がMustacheによって実際の値に置き換わり、以下のようなメッセージが生成されます。

```
#google_apps_script has created by @munieru_jp on 2018/12/7 18:17:13.
```

テンプレートを書く際の注意点として、APIから投稿する場合は`#チャンネル名`や`@ユーザー名`のように書いても単なる文字列として扱われてしまうので、それぞれ`<#チャンネルID>`や`<@ユーザーID>`のように書く必要があります。

### Slackにメッセージを投稿する
Slackにメッセージを投稿するには、[Incoming Webhooks](https://api.slack.com/incoming-webhooks)を使用します。
Incoming Webhooksでは、Slackで生成した一意なURLにHTTPリクエストを投げることで特定のチャンネルにメッセージを投稿することができます（このURLが漏洩すると投稿され放題になってしまうので、扱いには留意する必要があります）。
単純なテキストメッセージの場合は、以下のようなJSONを投げるだけです。

```json
{
  "text": "Hello World"
}
```

Google Apps Scriptでは、`UrlFetchApp.fetch()`関数でHTTPリクエストを送信することができます。

```js
function postToSlack_ (message) {
  var params = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    payload: '{"text":"' + message + '"}'
  }
  UrlFetchApp.fetch(WEBHOOK_URL, params)
}
```

## 動作イメージ
各BOTの実際の動作イメージは以下の通りです。

* 新規ユーザー通知BOT  
![45ddad15-c987-ce1a-1264-00ad12c429f9.png](https://qiita-image-store.s3.amazonaws.com/0/132608/45ddad15-c987-ce1a-1264-00ad12c429f9.png)
* 新規チャンネル通知BOT  
![4defaffe-3625-a0ed-577e-948509d4643e.png](https://qiita-image-store.s3.amazonaws.com/0/132608/4defaffe-3625-a0ed-577e-948509d4643e.png)
* 新規絵文字通知BOT  
![570555bf-60c1-3fd0-0958-68f6d9c7a7d2.png](https://qiita-image-store.s3.amazonaws.com/0/132608/570555bf-60c1-3fd0-0958-68f6d9c7a7d2.png)

弊社の場合はそれぞれ専用のチャンネルに投稿していますが、ワークスペースの規模によっては1つのチャンネルにまとめてもいいかもしれません。

## ソースコード
この記事で紹介したBOTのソースコードは、GitHubで公開しています。

* [munierujp/slack_new_user_notify_bot](https://github.com/munierujp/slack_new_user_notify_bot)
* [munierujp/slack_new_channel_notify_bot](https://github.com/munierujp/slack_new_channel_notify_bot)
* [munierujp/slack_new_emoji_notify_bot](https://github.com/munierujp/slack_new_emoji_notify_bot)