Cloud Functions for Firebase 🔥 + Bolt ⚡️で勉強会用のSlack Botを作ってみる

Firebase と Boltで勉強会用のSlack Botを作ったのでチュートリアル形式でまとめます。
無料版Slackで頑張っている全国の勉強会運営者の助けになれば..!!

~~※ 現状のBoltのFaaSでの利用には[いくつかの課題](https://github.com/slackapi/bolt/issues/361)があるようです。v2では改善されるようなので、その際にはv2版で記事更新します。~~~
2020/3/30にリリースされました。記事の内容もv2で対応しています。

# 作る物

- Slackでスラッシュコマンドを打つと、モーダルが開く
- フォームを入力し投稿ボタンを押すと、入力内容をフォーマットして投稿する

という簡単なBotです。
もくもく回の最初に「今日やることを投稿する」際に使うことをイメージしています。
処理は単純なので、いろいろ応用は効きそうです。

![Mar-17-2020 20-39-34 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/54d9ba50-ba6b-cf5b-5527-6d13682b4086.gif)

（有料版Slackならワークフローで一瞬で出来そうなのですが、自分の勉強会のSlackにはそんな便利な物はなく。。）


これから説明するコードは全て以下GitHubのリポジトリにあります。

https://github.com/kawamataryo/mito-engineer-bot/

# Slackの設定
まずSlack API側の設定です。Botアプリの作成から権限の追加などを行います。

## アプリの作成
以下リンクからCreate New Appを押してSlackボットを作成します。
任意のアプリ名、ワークスペースを入力してCreate Appを押してください。


https://api.slack.com/apps

<img width="1008" alt="スクリーンショット 2020-03-16 18.01.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/3d44a5d4-cf1f-25e9-78c8-56bfffcabc28.png">

## Scopeの設定

サイドバーのOAuth & PermissionsのScopesよりアプリがWorkspaceでどのような操作を行えるのかの権限を設定します。
Bot Token Scopesのプルダウンから以下を設定します。

- `chat:write` 
    アプリのメッセージ投稿を許可する
- `commands` 
    slashコマンドの読み取りを許可する
- `users:read` 
    ユーザー情報の読み取りを許可する
    

<img width="979" alt="スクリーンショット 2020-03-16 18.08.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/d8b1e75d-cd47-3c33-ca20-1b0b03c32740.png">

## Workspaceへの登録

同じくサイドバーのOAuth & Permissionsの一番上、Install App to Workspace を押してアプリをWorkspaceに追加してください。
先ほど設定したScopeについて確認が出るので許可を押してください。

<img width="984" alt="スクリーンショット 2020-03-16 18.09.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/1e01832f-008e-35bd-59b2-76c653256286.png">

## Tokenのメモ

アプリをWorkspaceに追加後、OAuth & Permissionsに表示されるBot User Access Token と、 Basic InformationのApp Credentialsに表示されるVerification Tokenをメモします。
次項でFunctions上のBolt Appの初期化で利用します。

（OAuth & Permissions > OAuth Tokens & Redirect URLs の Bot User Access Token）
<img width="1007" alt="スクリーンショット 2020-03-16 19.33.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/a75b3cab-f678-32a4-dfec-bf6fc5ff750d.png">



（Basic Information > App CredentialsのSigning Secret）
<img width="1015" alt="スクリーンショット 2020-03-16 20.42.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/abbf32a5-081d-4fdd-eb5a-bcf11a1ac7b4.png">


# Firebase の設定
次にFirebaseで実際にアプリのコードを書いてきます。

## プロジェクトの作成

以下からFirebaseのプロジェクトを作成します。

https://console.firebase.google.com/u/0/

作成後プランを**Blaze**に変更してください。Slack APIとの外部通信が発生するためです。
（無料プランはFunctions上でFirebaseサービス以外との通信ができない）


## 開発環境の構築

任意のディレクトリでFirebaseの開発環境を構築します。

```bash
# FirebaseのCLIが入っていない場合は事前に以下を実行してください
# npm i -g firebase-tools

firebase init
```

featureの選択ではCloud Functionsを選び、その他は任意の設定をしてください。
（サンプルコードはTypeScriptで書いています）

<img width="953" alt="スクリーンショット 2020-03-16 19.55.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/7a17725e-c33f-ee99-7d2b-9f21c7790a0d.png">

init後、functionsディレクトリが作成されます。
これからのコードは全てそちらを起点とします。

```
cd functions
```

## Boltのインストール
BoltはNode.js環境で動く SlackアプリのSDKです。
こちらに日本語ドキュメントもあります。
https://slack.dev/bolt/ja-jp/tutorial/getting-started

functionsディレクトリで以下を実行してBoltをインストールしましょう。


```
npm i @slack/bolt
```

## SlackのTokenを環境変数に設定
Slackアプリの初期化で利用するためSlackの設定でメモした `Signing Secret` 、`Bot User Access Token`,を環境変数に設定します

```
firebase functions:config:set slack.secret=xxxxxxxxxxxxxx
firebase functions:config:set slack.token=xxxxxxxxxxxxx
```

## Appの作成

functionsのsrcにslackディレクトリを作り`app.ts`を作成します

```bash
mkdir src/slack
touch src/slack/app.ts
```

app.ts上でslackアプリを作成します。
前項で設定した環境変数を`functions.config()`で取得してAppの初期化をしています。
`useMokumokuCommand`の部分は次に説明するmokumoku.tsで定義します。

この状態で、Receiverをfunctionsに渡すと`functionsのURL/events`でアプリにアクセスできます。


この初期化部分の実装は[こちらの記事](https://qiita.com/seratch/items/3e44f58a7c8ab41f21f0#bolt-%E3%81%AE%E5%88%9D%E6%9C%9F%E5%8C%96) を参考にさせて頂きました。




```src/slack/app.ts
import * as functions from "firebase-functions";
import { App, ExpressReceiver } from "@slack/bolt";
import { useMokumokuCommand } from "./commands/mokumoku";

const config = functions.config();

export const expressReceiver = new ExpressReceiver({
  signingSecret: config.slack.secret,
  endpoints: "/events",
  processBeforeResponse: true
});

const app = new App({
  receiver: expressReceiver,
  token: config.slack.token
});

useMokumokuCommand(app);
```

## mokumokuコマンドの作成

次に実際に処理をするmokumoku.tsを作っていきます。

```bash
mkdir src/slack/commands
touch src/slack/commands/mokumoku.ts
```

コードの完成形はこちらです。

- `/mokumoku`コマンドでモーダルを開く
- フォーム内容のPOSTを受けてフォーマットしたメッセージを送る

という処理を行っています。


```src/slack/mokumoku.ts
import { App } from "@slack/bolt";

const VIEW_ID = "dialog_1";

type User = {
  real_name: string;
  profile: {
    image_192: string;
  };
};

const createMessageBlock = (
  username: string,
  userIcon: string,
  profile: string,
  todo: string
) => {
  return [
    {
      type: "context",
      elements: [
        {
          type: "mrkdwn",
          text: `posted by *${username}*`
        }
      ]
    },
    {
      type: "divider"
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: `:memo: *自己紹介*\n${profile}\n\n\n:books: *今日やること*\n${todo}`
      },
      accessory: {
        type: "image",
        image_url: userIcon,
        alt_text: "user thumbnail"
      }
    },
    {
      type: "divider"
    }
  ];
};

export const useMokumokuCommand = (app: App) => {
  app.command("/mokumoku", async ({ ack, body, context, command }) => {
    await ack();
    try {
      await app.client.views.open({
        token: context.botToken,
        trigger_id: body.trigger_id,
        view: {
          type: "modal",
          callback_id: VIEW_ID,
          title: {
            type: "plain_text",
            text: "今日のもくもく"
          },
          blocks: [
            {
              type: "input",
              block_id: "profile_block",
              label: {
                type: "plain_text",
                text: "自己紹介"
              },
              element: {
                type: "plain_text_input",
                action_id: "profile_input",
                multiline: true
              }
            },
            {
              type: "input",
              block_id: "todo_block",
              label: {
                type: "plain_text",
                text: "今日やること"
              },
              element: {
                type: "plain_text_input",
                action_id: "todo_input",
                multiline: true
              }
            }
          ],
          private_metadata: command.channel_id,
          submit: {
            type: "plain_text",
            text: "投稿"
          }
        }
      });
    } catch (error) {
      console.error(error);
    }
  });

  app.view(VIEW_ID, async ({ ack, view, context, body }) => {
    await ack();
    const values = view.state.values;
    const channelId = view.private_metadata;
    const profile = values.profile_block.profile_input.value;
    const todo = values.todo_block.todo_input.value;

    try {
      // get user info
      const { user } = await app.client.users.info({
        token: context.botToken,
        user: body.user.id
      });
      // post chanel
      await app.client.chat.postMessage({
        token: context.botToken,
        channel: channelId,
        text: "",
        blocks: createMessageBlock(
          (user as User).real_name,
          (user as User).profile.image_192,
          profile,
          todo
        )
      });
    } catch (error) {
      console.error("post message error", error);
    }
  });
};

```

簡単に解説です。
`app.command()`でリッスンするスラッシュコマンドの登録と、実行時の処理を登録しています。第一引数に登録したいコマンドを、第二引数にコールバックで処理を書いていきます。

```src/slack/mokumoku.ts
app.command("/mokumoku", async ({ ack, body, context, command }) => { ... }
```

`app.view()`でモーダルの投稿結果を受け取る処理を登録しています。第一引数にapp.command内のモーダル表示処理app.client.views.openで設定したcallback_idを設定しモーダルとの接続を、第二引数のコールバックでメッセージの送信処理を書いています。

```src/slack/mokumoku.ts
app.view(VIEW_ID, async ({ ack, view, context, body }) => { ... }
```


## Functionsの設定

アクセスポイントとなるFunctionsを設定します。
http.onRequestにそのままexpressReceiver.appを渡せばOKです。とても簡単ですね。

```src/index.ts
import * as functions from "firebase-functions";
import { expressReceiver } from "./slack/app";

export const slack = functions.https.onRequest(expressReceiver.app);

```

## デプロイ

以下コマンドでアプリをFunctionsにデプロイします。

```
npm run build
npm run deploy
```

deployコマンド実行後表示されるFunctionsへのアクセスポイントのURLをメモしておいてください。
次項でSlackアプリとの連携設定で使います。


# Slackアプリとの連携

最後に前項でデプロイしたFunctionsとslackアプリを連携します。

## スラッシュコマンドの登録

https://api.slack.com/apps から作成したSlackアプリを選びサイドバーのSlash CommandsからCreate New Commandでコマンドを追加します。
Requst URLには先ほどメモした**FunctionへのアクセスポイントのURL/events**を入力してください。

<img width="1054" alt="スクリーンショット 2020-03-17 5.52.22.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/bd608281-f02c-1ebb-e8a1-d270820e35b4.png">

## Interactive Components の設定
モーダルからの投稿に対応するため Interactive Components を有効化にします
サイドバーのInteractive ComponentsからチェックボタンをOnにしてRequest URLに先ほどと同様の**FunctionへのアクセスポイントのURL/event**を入力して保存します。

これを設定することでモーダルの投稿ボタン押下後、内容がFunctionsにPOSTされます。

<img width="1217" alt="スクリーンショット 2020-03-17 6.01.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/19b864be-a2b2-8072-8f50-82d41c193edf.png">

※ 私はこれを最初設定せず、「なぜかモーダルひらくものの投稿できないな〜」と半日潰しました。。最終的に以下記事で紹介されていたSlackコミュニティ公式グループで質問してInteractive Componentについて教えてもらい解決しました。暖かいコミュニティに感謝:pray: 
https://qiita.com/girlie_mac/items/93538f9a69eb4015f951#comment-a983ea977482e78f209c


## 動作確認

コマンドを使いたいチャネルにBotを招待して、実際にSlack上でコマンドを叩いてみましょう。
無事動けば完成です :tada: 

![Mar-17-2020 17-54-23 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/86835c73-6ca1-6d88-1828-4030e6d7057d.gif)

あとアプリの名前や、アイコンはSlackアプリの設定画面のBasic InformationのDisplay Informationで、お好みに設定してください。

<img width="973" alt="スクリーンショット 2020-03-17 17.58.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114396/4636349d-22fb-4459-93b2-20bbf24b1cc8.png">



# 終わりに
以上、 「Cloud Functions for Firebase 🔥 + Bolt ⚡️で勉強会用のSlack Botを作ってみた」でした。
初めてのSlack Botでまだまだ分からないところばかりなので間違いなどあれば気軽にコメントで指摘お願いします。
今後もいろいろ機能を追加して勉強会のSlack Botを育てて行きたいです。



# 参考
以下記事大変参考にさせて頂きました! 良記事ありがとうございます。

- https://qiita.com/marogoma/items/e3003564c1b8b7b09e29
- https://qiita.com/seratch/items/3e44f58a7c8ab41f21f0
- https://qiita.com/daikiojm/items/759ea40c00f9b539a4c8
- https://qiita.com/girlie_mac/items/93538f9a69eb4015f951#comment-a983ea977482e78f209c