Slack次世代プラットフォームでミニアプリをつくったのでちょっと要点を解説

[Unipos Advent Calendar 2022](https://qiita.com/advent-calendar/2022/unipos)の記事です。


マルギットが倒せずエルデンリングを積んだあたりから老いを感じだしています。
最近は本流ポケモンゲームを数年ぶりに楽しんでいますが、タイプ相性がぜんぜん覚えられません。困ったのでクイズ形式で覚えていける(かもしれない)Slackアプリをつくりました。

![スクリーンショット 2022-12-25 11.29.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98265/be02a154-4d85-45f4-ee2d-5906f0bc526d.png)

ポケモンのタイプ相性クイズを出題してくれます

- botにメンションを飛ばすと、ポケモンタイプ相性クイズとして、問題メッセージが投稿される
- 回答選択肢ボタンが押されたら2通りの振る舞いをする
    - 正解→問題メッセージを更新し、答えを表示する。同時に、以降回答ができないようにする(終了)
    - 不正解→スレッドに誤答を送信する。回答は引き続きできる状態にする

せっかくだったので、これをSlack next-gen platformを使い実装してみましたので、要所を解説してみました。逆引きのようにだれかの参考になると幸いです。(Slack next-gen platformについては[こちらの記事](https://qiita.com/seratch/items/ecc16b845483c9d6f9e1)がとても参考になります)
ポケモンタイプ相性クイズアプリのつくりかたを直接解説するわけではないので、記事をちょっとだけ参考に、ぜひにご自身で実装してみてください:+1:

要所としては以下4点です。

1. botにメンションを飛ばすとワークフローが開始する(event trigger)
2. blockkitを含んだメッセージをチャンネルに投稿する
3. block kit actionのハンドリングをする
4. スレッドにメッセージを投稿する、メッセージを更新する

## 1. botにメンションを飛ばすとワークフローが開始する(event trigger)

Slack next-gen platformでは主にWorkflow、Trigger、Functionを定義・実装しアプリをつくっていきます。

今回のアプリはbotメンションをトリガーとするように定義しました。これはSlack next-gen platflomで取り扱われるトリガーのうちEvent triggersに区分されるものです。




[https://api.slack.com/future/triggers#types](https://api.slack.com/future/triggers#types)

イベントトリガーを設計・実装するステップとしては

1.取り扱えるイベントを確認し、必要なものをピックアップする
    
    [https://api.slack.com/future/triggers/event#supported-events](https://api.slack.com/future/triggers/event#supported-events)
    
    ポイントとして、イベントトリガーはChannel typeとWorkspace typeに二分されており、前者の場合はtrigger定義のなかでチャンネルIDのコーディングが必要です。つまりデプロイ段階でそのアプリが稼働できるチャンネルが決定している必要があります。
    

    
```typescript
type ChannelTypes = ObjectValueUnion<
  Pick<
    typeof TriggerEventTypes,
    | "AppMentioned"
    | "ChannelShared"
    | "ChannelUnshared"
    | "MessageMetadataPosted"
    | "PinAdded"
    | "PinRemoved"
    | "ReactionAdded"
    | "ReactionRemoved"
    | "SharedChannelInviteAccepted"
    | "SharedChannelInviteApproved"
    | "SharedChannelInviteDeclined"
    | "SharedChannelInviteReceived"
    | "UserJoinedChannel"
    | "UserLeftChannel"
  >
>;

type WorkspaceTypes = ObjectValueUnion<
  Pick<
    typeof TriggerEventTypes,
    | "ChannelArchived"
    | "ChannelCreated"
    | "ChannelDeleted"
    | "ChannelRenamed"
    | "ChannelUnarchived"
    | "DndUpdated"
    | "EmojiChanged"
    | "UserJoinedTeam"
  >
>;
```
- Channel type
        
    [https://github.com/slackapi/deno-slack-api/blob/main/src/typed-method-types/workflows/triggers/event.ts#L24](https://github.com/slackapi/deno-slack-api/blob/main/src/typed-method-types/workflows/triggers/event.ts#L24)
        
- Workspace type
        
    [https://github.com/slackapi/deno-slack-api/blob/main/src/typed-method-types/workflows/triggers/event.ts#L44](https://github.com/slackapi/deno-slack-api/blob/main/src/typed-method-types/workflows/triggers/event.ts#L44)
    
2.そのイベントから取り出せるプロパティを確認し、後のWorkflowを実行するのに必要なものを把握する
    
    [https://api.slack.com/future/triggers/event#supported-events](https://api.slack.com/future/triggers/event#supported-events)
    
![スクリーンショット 2022-12-25 22.56.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98265/c71ee53a-84f5-fd5c-67a1-5f079722e22f.png)
    
3.トリガーとして定義する
    
    ```tsx
    const mentionTrigger: Trigger<typeof ExaminationWorkflow.definition> = {
      type: "event",
      name: "mention_trigger",
      workflow: "#/workflows/examination_workflow",
      inputs: {
        channelId: {
          value: "{{data.channel_id}}",
        },
      },
      event: {
        event_type: "slack#/events/app_mentioned",
        channel_ids: ["{CHANNEL_ID}"],
      },
    };
    export default mentionTrigger;
    ```
    
    実際に実装していたコードのはこんなです。ExaminationWorkflowとは、今回ぼくが実装したWorkflowです。
    
    inputsとして、実行するWorkflowの入力となる値を定義しますが、このとき、手前のステップで確認したプロパティを参照する定義ができます。dataプロパティの子プロパティは上記のようにドットでつなぎ参照しましょう。
    
    manifest.tsへの権限の追加もお忘れなく(忘れててもslackコマンドでのデプロイ時におしえてくれます。やさしい)
    
    開発上のハマり話なのですが、イベントプロパティを参照する定義が間違っていると(例えば上記の `{{data.channel_id}}` を `{{data.channelId}}` にtpyoする)、デバッグ時にアプリケーションが一切トリガーされず、とくにエラーもでない状況に陥ります。vscodeで開発していたのですが、コードヒントも特に示されないため、なんだか動かんなとなったらひとまずtirgger定義をみてみるのがおすすめです。
    

## 2.  block kitを含んだメッセージをチャンネルに投稿する

メッセージの投稿や更新、削除など、基本的にslack web apiと同様のインターフェースで関数が提供されておりそれを使っていくことになります。

チャンネルへのメッセージ投稿は2つの方法があります。ひとつはWorkflow stepに追加して使う `Schema.slack.functions.SendMessage`です。チュートリアルのコードにも登場しました。

[https://api.slack.com/tutorials/tracks/hello-world](https://api.slack.com/tutorials/tracks/hello-world)

もうひとつはapi clientを通して使える `client.chat.postMessage`です。

[https://github.com/slackapi/deno-slack-api/blob/main/src/typed-method-types/chat.ts#L59](https://github.com/slackapi/deno-slack-api/blob/main/src/typed-method-types/chat.ts#L59)

ふたつの違いとして、前者はWorkflowに対してStepとしてかんたんに登録し扱えるのがよいところで、後者はFunctionの中から扱えるため、ロジックによるパラメータの調整がより柔軟です。

ぼくの今回のアプリでは `client.chat.postMessage`を使ったのですが、blockkitを使う場合もchat.postMessage apiと同様、 `blocks`プロパティを定義してあげればよいです。

```tsx
export default SlackFunction(
    PostQuestionFunctionDefinition,
    async ({ inputs, client }) => {
        const { question, options, channelId } = inputs;

        const buttonElements = options.map(o => {
            const labelText = `x${o}`
            return {
                type: "button",
                text: {
                    type: "plain_text",
                    text: labelText,
                },
                action_id: `${o}`,
            }
        })

        await client.chat.postMessage({
            channel: channelId,
            blocks: [
                {
                    type: "section",
                    text: {
                        type: "mrkdwn",
                        text: question
                    }
                },
                {
                    type: "actions",
                    block_id: "buttons",
                    elements: buttonElements,
                }],
        })

        return {
            completed: false,
        }
    }
)
```

(optionsには回答選択肢の’0’や’0.25’といった文字列が入ってきています)

blocksはany[]型で定義されておりエディタのサポートは受けられないですが、slack deploy実行時にバリデーションが走ってくれて、不正があれば教えてくれます。どのように不正かはおしえてもらえないようでした

ちなみに`completed: false`のみが定義されたobjectをリターンしていますが、これはFunction全体の処理がまだ終了しないことを示すものです

[https://api.slack.com/future/block-events#build](https://api.slack.com/future/block-events#build)

## 3. block kit actionのハンドリングをする

2から続き、block kit actionのハンドリングについてです。

Functionにチェーンする形でactionのハンドリングを `addBlockActionsHandler` 関数によって定義できますが、第一引数にハンドリング対象のaction idをとる必要があります

[https://api.slack.com/future/block-events#routes](https://api.slack.com/future/block-events#routes)

となるとコンパイル時にはaction idが事前に定義されている必要があるのかと思いましたが、引数の指定として文字列に加え正規表現
も指定できます。

![スクリーンショット 2022-12-25 23.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98265/7bef3e48-01d6-12cc-3b4e-e968a4713a40.png)

今回のアプリでは作問(わざタイプ、うけポケモンのタイプのランダムな決定)及び回答選択肢を実行時につくりたかったため、RegExpをつかい、すべての回答選択肢ボタンのハンドリングを正誤判定ロジックに流し、そのなかでaction idを参照できる作りにしてみました

```tsx
...
).addBlockActionsHandler(new RegExp(".+"), async ({ action, body, inputs, client }) => {
    const isRightAnswer = Number(action.action_id) === Number(inputs.answer);
...
```

## 4. スレッドにメッセージを投稿する、メッセージを更新する

スレッドへのメッセージ投稿は2と同様、`client.chat.postMessage`関数を使いました。更新は`client.chat.update`を使います

web apiと同様、timestampによって元となるスレッド・メッセージを指定する必要があります。

Functionの中で投稿されたメッセージを対象にするには、handlerのbodyプロパティを参照し、そのメッセージのtimestampを参照するとよいです

```tsx
...
).addBlockActionsHandler(new RegExp(".+"), async ({ action, body, inputs, client }) => {
    const isRightAnswer = Number(action.action_id) === Number(inputs.answer);
    if (isRightAnswer) {
        await client.chat.update({
            channel: inputs.channelId,
            ts: body.message?.ts,
            blocks: [
                {
                    type: "section",
                    text: {
                        type: "mrkdwn",
                        text: inputs.question
                    }
                },
                {
                    type: "section",
                    text: {
                        type: "mrkdwn",
                        text: `正解は x${inputs.answer} でした :+1::+1::+1: <@${body.user.id}>`
                    }
                },
            ]
        })

        await client.functions.completeSuccess({
            function_execution_id: body.function_data.execution_id,
            outputs: {}
        });
    } else {
        await client.chat.postMessage({
            text: `x${action.action_id} じゃないよ！`,
            channel: inputs.channelId,
            thread_ts: body.message?.ts,
        })
    }
})
```

## 番外編

- ポケモンタイプ相性と効き目の計算ですが、こちらのJSONを参考に、フェアリータイプだけじぶんで追加したものをつかいました
    
    [Pokémon type chart as a JSON.](https://gist.github.com/agarie/2620966#file-type-chart-json)
    
    ```tsx
    var effectiveness = TYPE_CHART["ground"][TYPE_ORDER["electric"]] * TYPE_CHART["ground"][TYPE_ORDER["water"]];
    ```
    
    計算もらくちんです。
    

# おわりに

ポケモン対戦はまだ手が出せていません。

それはさておきSlack next-gen platformを扱った感想ですが、Trigger、Workflow、Functionといった基本的なモジュールおよびフレームワークの使い方をキャッチアップするイニシャルコストこそあれど、それに乗っかってしまえば非常にシンプルに構想を練ってアプリケーションが作れる印象です。

なによりサーバーレスで、コマンドちょちょいでアプリケーションがデプロイできるのがハッピーです。 `slack trigger create`コマンドでトリガーをつくり `slack deploy`コマンドでアプリケーションをデプロイするだけです。アプリケーションのこと以外考えなくていいです。基本的にエディタとコンソールとSlackワークスペースでのデバッグ操作だけで開発が完結します。可能な限りシンプルな開発体験に興奮しました。権限までコードで定義するだけでよいので、slackのweb管理画面からポチポチ操作が不要になるのも個人的にはうれしかったです。

サーバレスなぶん、レイテンシに注目するとチューニングの余地は小さいのかな？という所感ではありますが、プロトタイピングに積極的に取り入れるところからはじめてみてはどうでしょうか。まだbeta提供ですが、今後がたのしみですね