ふりかえりのための備忘にSlackを活用する

原題「Cloud FunctionsをバックエンドにSlackのアクションを作る」

## :book: あらまし

チームでの開発に定期的なふりかえりは不可欠です。

私が所属するチームでは主にKPT方式でふりかえりを行っています。
しかし加齢のためか、ふりかえりを実施するタイミングでは
既にイテレーション内で起こったことを忘れ去っているという大きなProblemがあります。

そこでSlackのActions（アクション）機能を使って
思いついたその場でKeepやProblemを登録できるフォームを作成します。

## :gear: 仕組み

Slackの [アクション](https://api.slack.com/interactivity/actions) は、メッセージに対して任意の操作を定義する機能です。

例えば、チャット上でサイトの不具合の報告を受けたときに
その場でJIRAのチケットやGitHubのIssueを作成するような活用例があります。

公式の記事の [ダイアグラム](https://cdn-images-1.medium.com/max/1600/1*mNW-vmSmeegig_LuXBRwQA.png) にあるように
Slackアプリから送信されるペイロードを自前のアプリケーションで処理することにより
メッセージに紐づく各種データの処理を行うことができます。

本記事では [コチラ](https://api.slack.com/lang/ja-jp/actions) の記事を参考にインタラクティブなフォームを実装します。
運用コストを下げる為にバックエンドにはGCPのCloudFunctionsを利用します。

## :computer:  作業

### Slackアプリの作成

[参考記事](https://api.slack.com/lang/ja-jp/actions) の「アプリの作成と設定」に従います。

`Request URL` には自前のアプリケーションの受け口となるURLを記載します。
今回はトリガータイプをHTTPに設定したCloudFunctionsのURLを指定します。
（作成方法は後述のため、一旦仮のURLを設定して後で変更します）

作成が完了すると、メッセージ右上の「その他」メニューからアクションを選択できるようになります。
![Slack___test-for-slack-actions___mockmock.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43116/1f828ae8-fbf0-d3ba-5fe1-187dc61c41db.png)

### アプリケーションの実装

#### 関数の作成

GCPのコンソールからCloudFunctionsの関数を作成します。
ペイロードを受け取るため、トリガーはHTTPで設定します。
![Cloud_Functions_-_nature_-_Google_Cloud_Platform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43116/edba3097-a60b-aa4d-3b56-60dc061d055d.png)

（AWSのLambdaと比べて、単体でもHTTPリクエストを受け付けられるのが嬉しい）

なお、記事に書く都合上ソースコードをインラインエディタで記載しますが
運用上はCloud Source Repositoriesなどを活用した方が圧倒的に楽です。
動作はNode.js 10（ベータ版）で確認しています。

#### モジュールの準備

`package.json` に最低限必要なモジュールを記載しておきます。
CloudFunctions自体が機能を持っていることにより、 [参考記事](https://api.slack.com/lang/ja-jp/actions) の内容より少なめです。

```json
{
  "name": " slack-actions-kp",
  "version": "0.0.1",
  "dependencies": {
    "axios": "^0.19.0",
    "qs": "^6.9.1"
  }
}
```

#### 主な処理

アプリケーションの受け口として複数のURLを設定することはできないため
ペイロードから取得する `type` によって処理を分ける必要があります。

今回のアプリケーションが受け付けるリクエストは2通りあります。

1. アクションを選択されたときのリクエスト
2. フォームが送信されたときのリクエスト

```javascript
/**
 * Responds to any HTTP request.
 *
 * @param {!express:Request} req HTTP request context.
 * @param {!express:Response} res HTTP response context.
 */
exports.slackActionsKP = (req, res) => {
  const payload = JSON.parse(req.body.payload);
  const {type, submission} = payload;

  if (type === 'message_action') {
    // アクションが選択されたときの処理: Slack上でフォーム（ダイアログ）を開く
  } else if (type === 'dialog_submission') {
    // フォームが送信されたときの処理: 送信された内容をどこかに記録する
  }
}
```

リクエストの前者では、Slack上でフォーム（ダイアログ）を開く処理を行います。
ダイアログ上の項目の設定については [コチラ](https://api.slack.com/dialogs) が詳しいです。

```javascript
if (type === 'message_action') {
  const dialogData = {
    token: SLACK_ACCESS_TOKEN,  // 環境変数から読み込んで事前に復号する
    trigger_id: payload.trigger_id,
    dialog: JSON.stringify({
      title: 'KとPを忘れない',
      callback_id: 'do_not_forget_kp',
      submit_label: '登録する',
      elements: [
        {
          label: '種別（K/P）',
          type: 'select',
          name: 'type',
          value: 'Keep',
          options: [
            { label: 'Keep', value: 'Keep' },
            { label: 'Problem', value: 'Problem' }
          ],
        },
        {
          label: '内容',
          type: 'textarea',
          name: 'message',
          value: payload.message.text
        },
      ]
    })
  };

  // SlackのAPIでフォーム（ダイアログ）を開く
  // https://api.slack.com/methods/dialog.open
  axios.post('https://slack.com/api/dialog.open', qs.stringify(dialogData))
    .then((result) => {
      if(result.data.error) {
        res.sendStatus(500);
      } else {
        res.sendStatus(200);
      }
    })
    .catch((err) => {
      res.sendStatus(500);
    });
} ...
```

リクエストの後者では、送信された内容を記録する処理を行います。
今回はIncomingWebhook経由で、専用のチャンネルに内容を投稿します。

```javascript
} else if (type === 'dialog_submission') {
  // 結果だけ先に返す
  res.send('');

  // Slackにメッセージを投稿する
  const emoji = submission.type === 'Keep' ? ':ok_woman:' : ':no_good:'
  const data = {
    text: `${emoji} ${submission.type} ${submission.message}`
  };

  // WebhookのURLは環境変数から読み込んで事前に復号する
  axios.post(SLACK_WEBHOOK_URL, data)
    .then((result) => {
      console.log(result.data);
    })
    .catch((err) => {
      console.log(err);
    });
}
```

### 成果物

Slack上のフォームを通してK/Pを記録することができるようになりました。
メッセージ右上の「その他」からアクションを選択するとフォームが表示されます。
<img width="600" alt="sl.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43116/a075d905-f365-9338-26cd-1e7faea8fc74.png">

「登録する」を実行すると、それぞれこのように投稿されました。
<img width="500" alt="kp.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43116/9af71ea6-0e3f-eaf4-840c-86aaf8aacb01.png">

長ったらしい投稿でも、ソースコード込みでも、数ポチで記録できます。
また、`payload.message` には投稿者のIDも含まれるため
他のメンバーの投稿をそっと取り上げることもできます。※コードには未反映

## :warning: 注意書き

動くようにはなりましたが、運用の前にいくつか注意があります。

### アクセス元の認証

メイン処理の前段で、Slackからの正しいアクセスであることを確認する必要があります。
[参考記事](https://api.slack.com/lang/ja-jp/actions) の「リクエスト情報の認証」の箇所を参考に実装を加えます。

### 各種設定の暗号化

CloudFunctionsの環境変数にアクセストークンやシークレットを埋めますが
事前に [KMS](https://cloud.google.com/kms/docs/encrypt-decrypt?hl=ja#kms-howto-encrypt-nodejs) で暗号化を行い、使用前に復号して利用するようにします。

## :checkered_flag: むすび

メッセージをKeep/Problemとして記録するアクションを作成しました。

今回は他のチャンネルに投稿しただけですので
「コピペで良いのでは？」とか「ピン留めしたら？」という声が聞こえてきそうですが
スプレッドシートやDBにデータを保存すればもっと活用の幅は広がりそうです。

（私は付箋が好きです。備忘しても声を出して書き出すべきだと思います:relaxed:）

----

明日以降もLIFULLアドベントカレンダーは続きます。引き続きお楽しみに！
https://qiita.com/advent-calendar/2019/lifull