Slack Platform(Next-Gen)ハンズオン


# はじめに

Slack Platform(Next-Gen) v1.15.0を試してみた記事です。最新のバージョンと挙動が異なる場合があるのであらかじめご了承ください。

# クイックスタート(環境構築)

[https://api.slack.com/future/quickstart](https://api.slack.com/future/quickstart)

Step1: Automated Installerでやったらエラーになった（ログ取るの忘れた＆どんなエラーか憶えてないです、すみません🙏）ので、Manual Installatonでやったら上手くいきました！

Step2,3と順当に進み、

Step4で

```bash
$ slack trigger create --trigger-def "triggers/greeting_trigger.ts"
```

を実行したら、slack CLIのバージョンアップ(v1.15.0→v1.16.4)が促され、Yを押したら

```bash
🚫  Error: This Slack API method does not exist or you do not have permissions to access it. (unknown_method)

Error Details:

1: This method does not exist. Please reach out to the admin to approve hosting custom TOS (unknown_method)

Suggestion:

An admin or owner on your workspace needs to have accepted the Slack Platform Beta Terms of Service here: https://slack.com/admin/settings#hermes_permissions
```

と出て失敗。（使っているslackワークスペースのadmin権限持ってる人に許可してもらわないといけなかった）

無視して、もう一度コマンドを実行したらアプデがスキップされ成功しました。
（ここで表示される`Shortcut URL`をメモ）

Step5で`slack run`してローカルでサーバ立ち上げ、Step4でメモったURLをslack上に書き込むと
![%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%B3%E3%82%B7%E3%83%A7%E3%83%83%E3%83%88_2022-11-23_10.51.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70400/216bd2b8-f187-75dc-b105-e268c407241b.png)

ワークフローを実行するためのボタンが表示され、Botが動きました🙌
※`slack uninstall`で”hello_world” Appは消しておきました

# Slack Platformの概要

ハンズオンしてみて私がイメージした、Slack Botの基本的な概念はこんな感じです↓

- Triggers
    - [https://api.slack.com/future/triggers](https://api.slack.com/future/triggers)
    - その名の通り、Botが起動する最初のきっかけを定義する。
    - チャンネルに起動ボタンをつけてそれを押す（Link Triggers）とか、定期実行（Scheduled Triggers）などなど。
- Workflows
    - [https://api.slack.com/future/workflows](https://api.slack.com/future/workflows)
    - Triggersから呼ばれる、Bot処理の流れを定義するもの。
    - (例)トリガー発火→フォームを表示→データ処理→slackのチャンネルにメッセージ表示
- Functions
    - [https://api.slack.com/future/functions/custom](https://api.slack.com/future/functions/custom)
    - Workflowから呼ばれる、特定の処理をまとめて分割したもの。関数。Workflowからinputとして値をもらって、何らかの処理をしてoutputとしてWorkflowに返す。（input, outputは無くてもいい）
    - slackでよく使われる機能はBuilt-in functionとして予め定義されているのでそれを使うとベンリ：[https://api.slack.com/future/functions](https://api.slack.com/future/functions)

他にもこんなものも使います：

- Datastores
    - [https://api.slack.com/future/datastores](https://api.slack.com/future/datastores)
    - NoSQLのDB。Functionsから呼び出してデータを永続化できる。
- Manifest
    - [https://api.slack.com/future/manifest](https://api.slack.com/future/manifest)
    - Botの基本設定をする。Workflowsを新しく定義したらここを修正しないと読み込んでくれなかった（と思う）

# 実装例： シャッフルランチBot

会社で使えるBotを一つ作ってみたのでそれをご紹介しつつ、詰まったところも書いておきます〜

全コードはこちら：
[https://github.com/hirorocky/slack-future-hands-on](https://github.com/hirorocky/slack-future-hands-on)

## 背景

- 会社の人を3~4人にグループ分けして、ランチに行く文化がある。（シャッフルランチ）
- slackには全社員所属している。グループ分けをslack上で実施＆発表したい。
    - ※slack上にはシャッフルランチの選出から外したい社員もいるので、その辺は柔軟に設定できるようにしたい。

## 仕様
slack上のユーザーを登録して、登録されたユーザーをランダムにグループ分けするBot
- ランチメンバーを指定して、登録できる
- すでに登録されているランチメンバーを削除できる
- すでに登録されているランチメンバーを一覧表示できる
- すでに登録されているランチメンバーからランダムにグループを作れる
    - グループあたりの人数は3~4名
    - グループを作るときに3人グループと4人グループの数を指定できる
        - 計算でグループの数は分かるけど、あえて指定可能にすることで柔軟性を出したい

## 実装
### Triggers
- member_addition_shortcut
    - 「メンバー追加」ボタン
- member_removal_shortcut
    - 「メンバー削除」ボタン
- member_list_shortcut
    - 「メンバー一覧」ボタン
- member_grouping_shortcut
    - 「メンバー分け」ボタン

### Workflows
- member_addition_workflow
    - ランチメンバーを追加するフロー
        - ユーザーを複数入力できるフォームを表示
            - build-in functionのOpenFormで、Array型で受け取りたいときはこんな感じでいけました
                
                [https://api.slack.com/future/forms#element-schema:~:text=13557523.0005-,Schema.types.array,-title](https://api.slack.com/future/forms#element-schema:~:text=13557523.0005-,Schema.types.array,-title)
                
            
            ```tsx
            const inputForm = MemberAdditionWorkflow.addStep(
              Schema.slack.functions.OpenForm,
              {
                title: "追加するメンバー",
                interactivity: MemberAdditionWorkflow.inputs.interactivity,
                submit_label: "追加",
                fields: {
                  elements: [{
                    name: "members",
                    title: "メンバー",
                    type: Schema.types.array,
                    items: { type: Schema.slack.types.user_id },
                  }],
                  required: ["members"],
                },
              },
            );
            ```
            
        - member_addition_functionを、フォームで入力されたユーザーをinputにして呼ぶ
        - member_addition_functionのoutputをメッセージとして表示（実行したユーザーにだけ見えるメッセージ）
            - `Schema.slack.functions.SendEphemeralMessage`を使えば、みんなから見えないメッセージを送れます
- member_removal_workflow
    - ランチメンバーを削除するフロー
        - ユーザーを複数入力できるフォームを表示
        - member_removal_functionを、フォームで入力されたユーザーをinputにして呼ぶ
        - member_removal_functionのoutputをメッセージとして表示（実行したユーザーにだけ見えるメッセージ）
- member_list_workflow
    - 現在登録されているランチメンバーを一覧するフロー
        - member_list_functionをinput無しで呼ぶ
        - member_list_functionのoutputをメッセージとして表示（実行したユーザーにだけ見えるメッセージ）
- member_grouping_workflow
    - ランチメンバーをグループ分けするフロー
        - 3人グループと4人グループの数を入力できるフォームを表示
        - member_grouping_functionを、フォームで入力された数をinputにして呼ぶ
        - member_grouping_functionのoutputをメッセージとして表示

### Functions

- member_addition_function
    - ランチメンバーを追加する
- member_removal_function
    - ランチメンバーを削除する
- member_list_function
    - 現在登録されているランチメンバーを一覧する
- member_grouping_function
    - ランチメンバーをグループ分けする
        - 配列をランダムに並べ替える処理は以下のライブラリを使いました！
            - https://deno.land/x/earray@1.0.0/mod.ts
             ```ts
             const shuffled_members = EArray(members).shuffle();
             ```

### Datastores

- ランチメンバー LunchMenbers
    - シャッフルランチの候補となるメンバー
    - attribute
        - user_id: slack.user_id（user_idはslackワークスペースでユニークなので自然キーとして使える）

```tsx
const MemberDatastore = DefineDatastore({
  name: DATASTORE_NAME,
  primary_key: "user_id",
  attributes: {
    user_id: {
      type: Schema.slack.types.user_id,
    },
  },
});
```

しかし、上記実装だと上手くいかなかったです、（`id`が自動的に主キーになる？）
※[v1.17.0](https://api.slack.com/future/changelog#v1-17-0)で修正されたかもです

仕方なくattributeに`id`をいれて、`user_id`が重複しないようにFunctionで登録前に確認するようにしました。

```tsx
const MemberDatastore = DefineDatastore({
  name: DATASTORE_NAME,
  primary_key: "id",
  attributes: {
    id: {
      type: Schema.types.string,
    },
    user_id: {
      type: Schema.slack.types.user_id,
    },
  },
});
```

デバッグ用にDatastoresにアクセスできるCLIコマンドが用意されています！
```bash
slack datastore query '{"datastore": "members"}'     
```
詳しくは`slack datastore help`などで...

# おわりに

datastoreの挙動に謎が残りましたが、全体的には快適に開発を進められました。ローカルに開発環境を構築してデプロイの前にいろいろ試行錯誤できたり、Typescriptの強力な型推論があったりとDXは高いと思いました。

GASやLambdaなどの外部サービスに頼ること無く、Slackだけで完結できるので、（従来のslack botの実装方法と比べて）シンプルになっていてGoodだと思います👍