GASで作ったSlackBotを使ってチャンネルに流入させる仕組みを作った話

これは[TECOTEC Advent Calendar 2019](https://qiita.com/advent-calendar/2019/tecotec)の24日目の記事です。
残すところあと2日です！

# はじめに
ブロックチェーン事業部の飯田です。
さて、いきなりですがSlack便利ですよね！
弊社でもSlackを導入して丸2年ぐらいになります。
いまでは運用ルールなども策定されており安定して使われていると思われます。
ただ運用していくと色々と問題も出てきています。
その中で今回はこちらの問題に対応した話を紹介しようと思います。

# チャンネルに流入しない問題
弊社では基本的に自由にチャンネルが作成できるため、様々な目的のチャンネルが多数作成されており、`public`チャンネルで140個ほど確認できました。
`private`チャンネルも入れるともっと多くのチャンネルが存在することになります。
`public`チャンネルであれば自由に参加できるので、興味がある内容のチャンネルであればどんどん参加していくといいと思うのですが、実際複数のチャンネルに参加している人はあまりいないようです。
業務で使っているツールなので、業務連絡以外で使うのはどうなの？、っていう人もいたりするのかと思いますが、実はチャンネル数が多くて探すのが面倒、もしくは探し方がわからないっていう人の方が多いんじゃないでしょうか？

# 定期的にチャンネルを紹介する仕組みを作る
ということで、定期的にチャンネルを紹介する仕組みを作りました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217389/5611fa77-afd2-aac5-7b14-e5cf46578ec7.png)

弊社では、`雑談`・`共有`・`相談`といったプレフィクスをつけてチャンネルを作成するルールになっているので、チャンネル一覧をSlackAPIで取得して、プレフィクスでフィルタリングしてからランダムでチャンネルを1件取得してチャンネルに投稿する仕様にしました。

## 開発環境構築
今回、`SlackBot`を作成するにあたって`Google Apps Script`を利用しました。
スクリプトはローカルPCのエディタを使って`TypeScript`で記述し、Google謹製のCLIツール`Clasp`で反映するという構成で開発しました。
詳しい説明は参考資料を見ていただくとして、つまったところを説明したいと思います。

### SlackAPIトークン取得
どこで取得するのかが非常に分かりづらかったです。
[API トークンの生成と再生成](https://slack.com/intl/ja-jp/help/articles/215770388-API-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E7%94%9F%E6%88%90%E3%81%A8%E5%86%8D%E7%94%9F%E6%88%90)の「ボットユーザートークン」の手順にある「古いカスタムインテグレーション」ページで作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217389/3d24eaaf-1c32-2ce7-837e-e7f38f05d92d.png)

### TypeScriptの設定
こちらの「[clasp が Typescript をサポートした！](https://qiita.com/HeRo/items/f2ce057c6b1456e896ad)」を参考にしましたが、実は何も設定しなくていいです。
`clasp push`するだけで`ts`ファイルをトランスパイルしてくれます！

### ESLintとprettierの設定
コードの品質を保つために`ESlint`と`prettier`を導入しました。
色々試したところ最終的に以下のような設定になりました。

```.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
    es6: true,
    'googleappsscript/googleappsscript': true
  },
  globals: {
    SlackApp: false
  },
  extends: [
    'eslint:recommended',
    'plugin:prettier/recommended',
    'prettier',
    'prettier/@typescript-eslint'
  ],
  plugins: [
    'prettier',
    '@typescript-eslint',
    'googleappsscript'
  ],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module'
  },
  rules: {
    'no-console': 'off',
    'no-debugger': 'off',
    'no-unused-vars': 'off',
    'prettier/prettier': ['error', {printWidth: 120, semi: false, singleQuote: true}]
  }
}
```

要点としては以下になります。
関数を定義だけしてファイル内で使用しないので`no-unused-vars`のエラーがどうしてもでてしまうので`off`にしてあります。
また、後述するライブラリを利用すると以下のエラーがでるので、`globals`の設定を追加しています。

```console
  7:18  error  'SlackApp' is not defined  no-undef
```


## スクリプトの説明
まずはスクリプト全容です。

```Code.ts
const slackToken = PropertiesService.getScriptProperties().getProperty('SLACK_TOKEN')
const username = PropertiesService.getScriptProperties().getProperty('USERNAME')
const iconUrl = PropertiesService.getScriptProperties().getProperty('ICON_URL')
const channelToPostRecommendations = PropertiesService.getScriptProperties().getProperty(
  'CHANNEL_TO_POST_RECOMMENDATIONS'
)
const slackApp = SlackApp.create(slackToken)

function postSlack(channelId, message, options) {
  options.username = username
  options.icon_url = iconUrl
  slackApp.postMessage(channelId, message, options)
}

function postRecommendChannel() {
  const { channels } = slackApp.channelsList(true)
  const filteredChannels = channels.filter(filterChannel).map(channel => {
    return {
      id: channel.id,
      name: channel.name,
      description: channel.purpose.value.replace(/\r?\n/g, '').slice(0, 100)
    }
  })
  const channel = filteredChannels[Math.floor(Math.random() * filteredChannels.length)]
  const attachments = [
    {
      pretext: '今日のおすすめチャンネル！',
      color: '#ffa500',
      title: `<#${channel.id}|${channel.name}>`,
      text: `${channel.description}`
    }
  ]
  const options = {
    attachments: JSON.stringify(attachments)
  }
  postSlack(channelToPostRecommendations, '', options)
}

function filterChannel(channel) {
  if (!channel.purpose.value) {
    return false
  }

  const name = channel.name
  if (name.match(/^雑談-/)) {
    if (name !== '雑談-全体') {
      return true
    }
  } else if (name.match(/^共有-/)) {
    if (name !== '共有-全体') {
      return true
    }
  } else if (name.match(/^相談-/)) {
    return true
  }
  return false
}
```

以下に上記コードで取り入れた内容を説明していきます。

### ライブラリを使用する
`SlackAPI`との通信を楽にするために`SlackApp`というライブラリを使いたかったので、こちらの「[[GAS]Claspでライブラリを使う方法](https://note.com/miraisouzoukan/n/n1dd76f67aaf9)」方法を参考にして導入しました。
上記記事でも説明されていますが、Google Apps Scriptのコンソールでライブラリの設定をしてから`clasp pull`するのが簡単だと思います。

### コードに埋め込みたくない情報の設定
SlackAPIのトークンなど、コードに直接書きたくない情報については、`PropertiesService.getScriptProperties()`を利用することでスクリプトのプロパティから取得できます。
設定方法はこちらの「[【初心者向けGAS】プロパティストアの概要とスクリプトプロパティの編集方法](https://tonari-it.com/gas-property-store/)」を参考にしました。


### attachmentsの設定
なかなかできなくて困ったのですが、こちらの「[お天気ババアをリッチにした話](https://qiita.com/bbq-all-stars/items/602347dc5779184474f9)」にかかれているように、`attachments`に設定する際に`JSON.stringify()`するだけでした。

## まとめ
今回作成したボットで定期的に全員が参加しているチャンネルへメッセージを送るようにしてみたところ、それなりに流入しているようです。
`Google Apps Script`は久しぶりに使いましたが、`Clasp`のようなツールがあったり`TypeScript`で記述できたりとかなり進化していてびっくりしました。
今回は時間がなく試せませんでしたが、`Jest`でテストを書くこともできるようなので、今度はテストの作成にも挑戦したいです。
`Google Apps Script`を利用するモチベーションがあがったのでまた何か作りたいと思います！

## 参考資料

[API トークンの生成と再生成](https://slack.com/intl/ja-jp/help/articles/215770388-API-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE%E7%94%9F%E6%88%90%E3%81%A8%E5%86%8D%E7%94%9F%E6%88%90)
[google/clasp](https://github.com/google/clasp)
[GAS ビギナーが GAS を使いこなすために知るべきこと 10 選](https://qiita.com/tanabee/items/2c51681396fe12b6a0e4)
[clasp が Typescript をサポートした！](https://qiita.com/HeRo/items/f2ce057c6b1456e896ad)
[[GAS]Claspでライブラリを使う方法](https://note.com/miraisouzoukan/n/n1dd76f67aaf9)
[【初心者向けGAS】プロパティストアの概要とスクリプトプロパティの編集方法](https://tonari-it.com/gas-property-store/)
[お天気ババアをリッチにした話](https://qiita.com/bbq-all-stars/items/602347dc5779184474f9)
[Step by Stepで始めるESLint](https://qiita.com/howdy39/items/6e2c75861bc5a14b2acf#globals%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E8%BF%BD%E5%8A%A0)