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

はじめに

この記事は前回取材してもらった記事 社内部活動でfondeskのカスタマイズ版とSlackチャンネル「TLけいさつ」を作った僕の野望 から技術的な要素をもっと細かく書いたものです

使用技術

なんで作ったか

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

e.g.

閃き

Slack のプロフィールと同期してくれたらいいのでは? と思い、fondesk がいるチャンネルのメンバーのプロフィールを定期的に同期するハンドラーを書きました(Cron)

実装編

HTTP リクエストを実装

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

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 で動かしているものです

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

image.png

ちょっと困ったこと

Cron の schedule ですが微妙にやりたいことができなかったです 例えば弊社では基本的に受電する機会は平日に限られます。 なので平日の営業時間 9:00-18:00 までが受電する時間帯です プロフィールの更新作業もこの時間と同じにしたかったのですが、GAE の Cron ドキュメントを見ると、できないようです

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

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

参考

プロフィールを DB に保存する

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

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

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

image.png

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

image.png

このように会社ごとで命名規則などがあると思うのでその点は調整可能なようにしています (これは config ファイルなどを別で管理する予定

    // 検索対象に含めたくない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

Event Subscriptions

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

Before

image.png

After

image.png

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

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 に問合せます

    const splitedText = payload.text.match(/あて先:(.+)\s/);
    const targetMember = splitedText.length > 1 ? splitedText[1] : "";
    const hitUser = await getFirestore(targetMember);

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

誰も見つからなかったとき

image.png

複数人のメンション

image.png

さいごに

先人たちに感謝! そしてお気軽に PR やイシューを是非お願いします〜 75asa/fondeSlack