Help us understand the problem. What is going on with this article?

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

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

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

作る物

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

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

Mar-17-2020 20-39-34 (1).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

スクリーンショット 2020-03-16 18.01.49.png

Scopeの設定

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

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

スクリーンショット 2020-03-16 18.08.56.png

Workspaceへの登録

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

スクリーンショット 2020-03-16 18.09.43.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)
スクリーンショット 2020-03-16 19.33.28.png

(Basic Information > App CredentialsのSigning Secret)
スクリーンショット 2020-03-16 20.42.36.png

Firebase の設定

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

プロジェクトの作成

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

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

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

開発環境の構築

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

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

firebase init

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

スクリーンショット 2020-03-16 19.55.57.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 SecretBot User Access Token,を環境変数に設定します

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

Appの作成

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

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

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

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

この初期化部分の実装はこちらの記事 を参考にさせて頂きました。

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を作っていきます。

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/eventを入力してください。

スクリーンショット 2020-03-17 5.52.22.png

Interactive Components の設定

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

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

スクリーンショット 2020-03-17 6.01.02.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

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

スクリーンショット 2020-03-17 17.58.53.png

終わりに

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

参考

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

ryo2132
Frontend engineer / フルリモートワーク / 元消防士🚒 / 一児の父 / Ruby / Typescript / Vue.js / Firebase
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away