Firebase を駆使して Slack の slash command をつくってみた

みなさんこんにちは。Aidemyの刀根です。今回は弊社slackでslash commandを一つ追加してみました。

2018/12/6追記
[さらに改良した。](https://qiita.com/Swordroot_M/items/43e24eee567b1fdae8ad)のでよかったらどうぞ。
本稿の内容をベースにしたものなので一回本稿を読んだほうがいいかも

#何したの？
実は弊社には集中力を持続させるためちょいちょいお昼寝する方が多いのです。
しかし、寝すぎてしまう人もちらほら・・・

そこで、お昼寝する前に、「15分寝る」場合は `/sleep 15`とコマンドを叩いて、時間が経ったら `@here 〇〇さんを起こしましょう`と通知できるようにしました。（それをみたひとに起こしてもらうという運用で）

#構成

- [Cloud Functions for Firebase](https://firebase.google.com/docs/functions/?hl=ja)
    - Typescript で記述
- Slack
    - incoming webhook
    - slash command

サーバーレスは最高。

#事前にやっておくこと
##チャンネルIDの取得
[conversations.listのテスター](https://api.slack.com/methods/conversations.list/test)
テスターを利用して、通知を行うチャンネルのIDを取得しておきます。

##incoming webhookの設定
リマインダーを投稿するチャンネルのincoming webhookを設定して起きます。

##slash commandの設定
オリジナルのコマンドを動かせるように設定します。今回は `/sleep` にしました。


#実装
今回はfunctionふたつ用意するだけなのでとてもシンプルです。

一つはhttpリクエストで発火する関数、もう一つはcronjobで発行されるPubSubトピックメッセージで発火する関数です。
詳しくはこちらの記事を見てね。[FirebaseでTwitterをbot化する](https://qiita.com/Swordroot_M/items/3b9ec5129c323ef32ee0)

流れとしては、
1. `/sleep` をslackで実行すると,httpリクエスト(POST)が飛ぶ
2. httpリクエスト(POST)が飛んできたらリマインダをデータベースに登録。
3. 毎分実行されるcronジョブでリマインダを投稿、データベースからリマインダ情報を削除

という流れになります。

実際に作ったソースコードがこちら

```typescript:index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import * as axios from 'axios';
import {myMoment} from './myMoment';//moment-timezoneを利用して作成したクラス。後述
import * as shortid from 'shortid';

// slackのincoming webhookのurl
const URL = 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXX';

// dbに繋がるようにする
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();

// httpリクエストで発火する関数
export const setReminder = functions.https.onRequest(async (req,res) => {
  console.log('fire!');
  try {
    if(req.method === 'POST'){
      const min = parseInt(req.body.text || 30);
      const user_id = req.body.user_id;
      const remindTime = myMoment().add(min, 'minutes').format('X');
      await db.collection('reminders').doc(shortid.generate()).set({
        user_id,
        remindTime
      });
      res.send('reminder is set.');
    }else{
      throw new Error('only post method is accepted');
    }
    
  } catch (error) {
    console.error('error');
    res.status(500).send(error);
  }
})

// cronjobで発火する関数
export const notifyReminder = functions.pubsub.topic('minutes-tick').onPublish(async (event) => {
  console.log('fire!');
  try {
    const now = myMoment().format('X');
    const queryData = await db.collection('reminders').where('remindTime', '<=', now).orderBy('remindTime', 'asc').get();
    const promises = queryData.docs.map(element => {
      const key = element.id;
      const data = element.data();
      return axios.default.post(URL, {
        text: `<!here> <@${data.user_id}>さんを起こしましょう`
      }).then(async () => {
        console.log(`finish to remind ${data.user_id}`);
        await db.collection('reminders').doc(key).delete();
      }).catch(error => {
        throw error;
      });
    });
    await Promise.all(promises);

  } catch (error) {
    console.error('error');
  }
});

```

```typescript:myMoment.ts

import * as moment from 'moment-timezone';

moment.tz.setDefault("Asia/Tokyo");

moment.locale("ja", {
  weekdays: [
    "日曜日",
    "月曜日",
    "火曜日",
    "水曜日",
    "木曜日",
    "金曜日",
    "土曜日"
  ],
  weekdaysShort: ["日", "月", "火", "水", "木", "金", "土"]
});

export const myMoment = moment;
```

#動作結果
![スクリーンショット 2018-10-11 10.37.38.png](https://qiita-image-store.s3.amazonaws.com/0/153926/fd9b99fc-bc5f-d612-0a8f-b789baf4753f.png)
ちゃんと動作してる！やったね！

#まとめ
* slackにコマンドを追加した
* 意外と簡単に追加できる
* とりあえずバックエンドは迷ったら `#Firebaseしろ`