fondesk のメンション機能を Slack Bolt Framework を使っていい感じにした話

# はじめに
この記事は前回取材してもらった記事 [社内部活動でfondeskのカスタマイズ版とSlackチャンネル「TLけいさつ」を作った僕の野望](https://note.com/tambourine_75/n/n2163b752786e)  から技術的な要素をもっと細かく書いたものです


# 使用技術
- 言語
    - NodeJS
    - TypeScript
- FrameWork
    - [Slack Bolt for JS](https://github.com/SlackAPI/bolt-js) 
- PaaS
    - [Goole App Engine a.k.a. GAE](https://cloud.google.com/appengine/?hl=ja&utm_source=google&utm_medium=cpc&utm_campaign=japac-JP-all-ja-dr-bkws-all-super-trial-e-dr-1009137&utm_content=text-ad-none-none-DEV_c-CRE_314712311199-ADGP_Hybrid+%7C+AW+SEM+%7C+BKWS+~+T1+%7C+EXA+%7C+Compute+%7C+1:1+%7C+JP+%7C+ja+%7C+app+engine+%7C+gae+%7C+en-KWID_43700030990911706-kwd-303155455422&userloc_1009523-network_g&utm_term=KW_gae&gclid=EAIaIQobChMIiOS0x8mp7QIVjbaWCh39kAhCEAAYASAAEgJKKvD_BwE) 
- DB
    - [Firebase Cloud Firestore](https://firebase.google.com/docs/firestore)


# なんで作ったか
既に同じような機能をもつ bot はあったけどどれもメンバー表を別で持たないといけなかった
なので都度更新しないといけないかったりする億劫さないしは運用作業が必要になっちゃう。。

## e.g. 
- スプレッドシートを使った場合
    - [fondeskのSlack通知に対してメンションを追加するBOTを作った
](https://qiita.com/neeton_iwasaki/items/f32baa321e3353263b18)
- S3
    - [[作ってみた]fondeskからSlackへの受電通知にメンションをつけるbot
](https://qiita.com/myonoym/items/0541c0e74e0006a71870)

# 閃き
<span style="font-weight: bold">Slack のプロフィールと同期してくれたらいいのでは？</span>
と思い、fondesk がいるチャンネルのメンバーのプロフィールを定期的に同期するハンドラーを書きました（Cron）

# 実装編

## HTTP リクエストを実装
Cron をどのようにしようかと迷ってたんですが、今回 PaaS を GAE にしてることもあって
HTTPリクエストを受け取れるエンドポイントを作ることにしました。
Slack Bolt Framework は Express のラッパーなので Express Receiver が使えます
以下のようにしてあっという間に呼び出せます

```typescript
import { ExpressReceiver } from "@slack/bolt";

const receiver = new ExpressReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
});

receiver.app.get(`/register`, (req, res) => {
    res.sendStatus(200);
    // チャンネルメンバーを取得
    const channel = new Channel(app);
    // 更新
    channel.putChannelMemberFirestore();
});
```

## Cron を実装
GAE の場合は cron.yaml を作成します
そして以下のコマンドを打つと cron を設定できます

`$ gcloud app deploy app.yml cron.yml`

以下は実際に [fondeSlack](https://github.com/75asa/fondeSlack) で動かしているものです

```cron.yaml
cron:
  - description: upsert slack profile into firestore
    url: /register
    schedule: every 1 hours from 9:00 to 18:00
    timezone: Asia/Tokyo
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/ed672336-4ec8-f63b-d92b-3117d8fb6ee2.png)


### ちょっと困ったこと
Cron の schedule ですが微妙にやりたいことができなかったです
例えば弊社では基本的に受電する機会は平日に限られます。
なので平日の営業時間 9:00-18:00 までが受電する時間帯です
プロフィールの更新作業もこの時間と同じにしたかったのですが、[GAE の Cron ドキュメント](https://cloud.google.com/appengine/docs/standard/python/config/cronref#schedule_format)を見ると、できないようです

↓ こうしたかった... 
（平日だけ9:00-18:00に1時間に一回更新）

```yaml

cron:
  - description: upsert slack profile into firestore
    url: /register
    schedule: every mon tue wed thu fri 1 hours from 9:00 to 18:00
    timezone: Asia/Tokyo
```

#### 参考
- [cron ジョブの schedule を定義する](https://cloud.google.com/appengine/docs/standard/python/config/cronref#defining_the_cron_job_schedule)
- [schedule の書式: カスタムの間隔](https://cloud.google.com/appengine/docs/flexible/nodejs/scheduling-jobs-with-cron-yaml#formatting_the_schedule)

## プロフィールを DB に保存する
今回、DB はさくっと作りたかったので Firestore を使ってます
構成としては、Slack のプロフィールを DB に保存しておいて、fondesk からメッセージがきたら DB と照らしあわせて、対象のユーザの SlackID でメンションする形です

### Slack 命名規則だったり除外したいプロフィール項目だったり

プロフィール項目は特定の項目は除外するようにしてます
弊社でいうと部署ですね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/5e700a09-e279-b16b-f7ca-d7e2615b8663.png)



弊社では Slack の本名には命名規則があり、_Asato Nago / 名護 朝人_ のようにしています

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/9ef0d3ff-0619-7f40-9a46-96e4490fb115.png)

このように会社ごとで命名規則などがあると思うのでその点は調整可能なようにしています
（これは [config ファイルなどを別で管理する予定](https://github.com/75asa/fondeSlack/issues/15)）

```typescript
    // 検索対象に含めたくないslackのプロフィール項目名
    private IGNORE_CUSTOM_FIELDS_LABEL = ["部署"];

    private splitWords = (data: string): string[] => {
        if (!data) return [];

        // チームによってSlackプロフィールの設定ルールがあると思うのでここは区切りたい文字で
        const specialChar = /\/||／/;
        const result = data
            .split(/\s+/)
            .filter(item => item.replace(specialChar, ""));
        console.log({ data });
        return result;
    };

    private getCustomFields = (data: fields): string[] => {
        if (!data) return [];
        return Object.keys(data).flatMap(element => {
            const field = data[element];
            if (this.IGNORE_CUSTOM_FIELDS_LABEL.includes(field.label)) {
                return "";
            }
            return this.splitWords(field.value);
        });
    };
```

### 実際の Firestore Console
駆け足で作ったのと初めての Firestore なので DB 構成などはもっと良くなると思います
何か tips などあればぜひ教えてほしいです 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/f2d025fe-54ab-755a-04a8-ff9e9912642d.png)


## Event Subscriptions
さて、最後は fondesk からのメッセージから DB に問合せてメンションする処理です
12/2 から fondesk のペリカンアップデートによって Slack の通知方法が変わりました
具体的には 旧い attachment から block kit を使うようになり、とてもみやすい＆わかりやすい内容となってました

### Before
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/76503598-f265-e1d5-5b6b-75c22d931451.png)

### After
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/0461ff47-6a65-4036-a98a-4935718c22d2.png)


なお、コード側では fondesk のメッセージにのみ反応’するようにしています

```typescript
app.message(/^(.*)/, async ({ context, message: payload }) => {
    console.log({ payload });

    if (
        payload.subtype !== "bot_message" ||
        !payload.blocks ||
        !payload.blocks.length
    ) {
        console.log("🥺 not fondesk message");
        return;
    }
```

誰に対してのメッセージかは正規表現で抜き出し、DB に問合せます
```typescript
    const splitedText = payload.text.match(/あて先:(.+)\s/);
    const targetMember = splitedText.length > 1 ? splitedText[1] : "";
    const hitUser = await getFirestore(targetMember);
```

あとは、複数人ヒットした場合はメンションを追加したり、また誰も見つからなかった場合のみ `@here` をつけるようにしたり、メンションすべき人が見つかった場合はスレッドメッセージにしたりしています

###  誰も見つからなかったとき
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/b1fea8a3-dcb0-1ea9-6715-eeef538ad5a0.png)

###  複数人のメンション
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230031/47d5c5cf-d2c1-99eb-f7a4-d0ce558ecf4d.png)


# さいごに
先人たちに感謝！
そしてお気軽に PR やイシューを是非お願いします〜
[75asa/fondeSlack](https://github.com/75asa/fondeSlack)