Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

FirebaseでSlack Slash Commandsを楽に運用する

ForSchool事業部開発グループの松田です。

弊社ではSlackを導入しており、いわゆるChatOpsに活用しています。
Slackにはさまざまな便利機能がついていますが、その中の一つにSlash Commandsというものがあります。
今回はこのSlash Commandsを自作して、そのバックエンドにFirebaseを利用した例をご紹介します。

Slack Slash Commandsとは

Slack内で /command のようにスラッシュから始まるコマンドを発言することで、特定のエンドポイントにリクエストを送信する機能です。
実装次第では /command sugoi argument などのように引数をとることもできるので汎用性が高いです。
標準機能として /remind/invite など複数のコマンドが定義されています。
Built-in slash commands – Slack Help Center

Firebaseとは

ここのところ流行っている感の強いモバイルプラットフォームです。
Googleの展開しているプラットフォームで、最近では国内の導入事例も見かけるようになりました。
認証周りをやってくれるAuthenticationや、ファイルホスティングのCloud Storageなどいくつかのサービスがありますが、
今回はJSの関数をさまざまなトリガーで実行できるCloud Functionsと、DBを提供するFirestoreを利用して簡単なコマンドを実装してみました。

実際に作ってみる

今回は弊社のボードゲーム部で使用する /boardgame コマンドを実装してみます。
仕様は /boardgame search players 4 の入力からDBを検索しプレイ人数に合致するボードゲームを返すコマンドとします。
Slash Commandsはコマンドが発行されると設定したURLにリクエストを投げるので、その先をCloud Functionsで受ける感じでやっていきます。

Slack AppとFirebaseプロジェクトを新規登録

Slack AppとFirebaseのプロジェクトを新規に登録します。
名前などいい感じにしましょう。

Cloud Functionsを仮デプロイ

firebase/firebase-toolsを使って、ローカルにFirebaseのプロジェクトと連携するフォルダを作ります。
初回設定時に使用するサービスを選択しますが、前述の通りFunctionsとFirestoreを選択します。
f:id:matsudap:20180817154806p:plain Functionsを選択するとJSとTSのどちらで書くかを聞かれますが、なんとなくTSにしました。
この時点で ./functions/src/index.tshelloWorld というHTTPリクエストがトリガーの関数が定義されているので、 boardgame に改名して、以下のコマンドを実行します。

$ firebase deploy --only functions

すると、デプロイ結果としてトリガーになるURLが表示されるので、ブラウザ等でリクエストをして Hello, World! と表示されればここまでは完了です。

自作Appをワークスペースに追加する

先程作ったSlack Appの設定メニュー「Slash Commands」からコマンドを登録します。
Request URLには先ほどデプロイしたCloud Functionsに割り当てられたURLを設定しておきます。
あとはよしなに設定します。
f:id:matsudap:20180817154828p:plain

Slash Commandから呼ばれる関数を実装する

とりあえず手元にボードゲームのリストを用意しました。

{
  "title": {
    "ja": "アグリコラ",
    "default": "Agricola"
  },
  "minPlayers": 1,
  "maxPlayers": 5,
  "minPlayingTime": 30,
  "maxPlayingTime": 150,
  "properAge": 12
},
{
  "title": {
    "ja": "デッドオブウィンター",
    "default": "Dead of Winter: A Crossroads Game"
  },
  "minPlayers": 2,
  "maxPlayers": 5,
  "minPlayingTime": 60,
  "maxPlayingTime": 120,
  "properAge": 13
},
...

これを適当なスクリプトを書いてFirestoreに突っ込みました。
最近使えるようになった配列の array-contains オペレータを使ってみたかったので、
Firestoreのトリガーで、minPlayersとmaxPlayersを下限と上限にした数値の配列をplayersフィールドに追加するようにしています。

そして実装ですが、公式のチュートリアルを参考にざっくりと以下のようになりました。1

import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin'

admin.initializeApp()
const db = admin.firestore()

class RequestError extends Error {
  code: number;
}

const commands = ['search'];

const searchFunction = (args) => {
  return new Promise((resolve, reject) => {
  })
}

export const boardgame = functions.https.onRequest((req, res) => {
  return Promise.resolve()
    .then(() => {
      if (req.method !== 'POST') {
        const error = new RequestError('Only POST requests are accepted');
        error.code = 405;
        throw error;
      }
      if (!req.body || req.body.token !== functions.config().slack.token) {
        console.log(req.body, functions.config().slack.token);
        const error = new RequestError('Invalid credentials');
        error.code = 401;
        throw error;
      }
      const [command, ...args] = req.body.text.split(' ');
      if (command == 'search') {
        let query:FirebaseFirestore.Query | FirebaseFirestore.CollectionReference = db.collection('games')
        if (args.includes('players')) {
          const playersIndex = args.indexOf('players') + 1;
          const players = args[playersIndex];
          query = query.where('players', 'array-contains', parseInt(players));
        }
        query.limit(3).get().then((querySnapshot) => {
          resolve({
            text: 'Boardgame Search Result',
            attachments: querySnapshot.docs.map((doc) => {
              const game = doc.data();
              return {
                title: game.title.ja,
                text: game.title.default,
                fields: [
                  {
                    title: 'Players',
                    value: `${game.minPlayers} ~ ${game.maxPlayers}`,
                    short: true,
                  },
                  {
                    title: 'Time',
                    value: `${game.minPlayingTime} ~ ${game.maxPlayingTime}`,
                    short: true,
                  },
                  {
                    title: 'Age',
                    value: game.properAge,
                    short: true,
                  }
                ]
              }
            }),
          });
        })
      } else {
        return new Promise((resolve, reject) => {
          resolve({
            text: 'Unknown command!',
            attachments: [],
          })
        })
      }
    })
});

functions.config().slack.token にはSlack App Basic Information内のVerification Tokenを予め設定しておきます。

Cloud Functionsにデプロイする

再度を以下のコマンドを実行してCloud Functionsにデプロイします。

$ firebase deploy --only functions:boardgame

Slach Commandを実行する

Slack Appを追加したワークスペースのチャンネル上で以下を発言します。

/boardgame search players 1

f:id:matsudap:20180817154858p:plain

便利ですね。

まとめ

Firebaseは運用コストが低いので、こういったピンポイントなAppのバックエンドとしては最適だと思いました。
FirebaseのいろいろなサービスもCloud Functionsからスッと使えるので対応できる幅も広そうです。
一方で、今回使用したCloud FunctionsとFirestoreは(2018/08/20現在)ベータでの提供なので留意してください!

また、弊社では就業後にボードゲーム部が不定期活動中です!
ボードゲームがしたい、Firebaseで業務改善したいエンジニアをどしどし募集しております〜。
まずはお気軽にオフィスに(ボードゲームを)遊びに来てください〜。
採用情報 | スタディプラス株式会社

www.wantedly.com


  1. このチュートリアルはFirebaseではなくGoogle Cloudのものなので、設定値の取得方法など微妙に差異があるので注意してください。