Bolt + jsx-slack + TypeScript を使って Slack App をつくる

- サクッと Slack App 書いてみたい
    - :point_right: [Bolt | Slack](https://api.slack.com/tools/bolt)
- [Block Kit](https://api.slack.com/block-kit) でリッチなUIを作りたいけど、JSON 書きたくない
    - :point_right: [speee/jsx-slack: Build JSON object for Slack Block Kit surfaces from readable JSX](https://github.com/speee/jsx-slack#jsx-slack)
- Slack App くらいの規模なら...という気持ちもあるが、補完や予期しない型ではじいてくれるのは嬉しい
    - :point_right: [microsoft/TypeScript: TypeScript is a superset of JavaScript that compiles to clean JavaScript output.](https://github.com/microsoft/TypeScript#typescript)
    

となるため、今後はこんな構成になりがちなのではないでしょうか:thinking:

がまだそんなに事例を見ていないのでちょっとやってみて記事に起こしてみました。
(まとめが追いついておらずだいぶ半端になっている)

私自身まだ色々試行錯誤中なため、色々ご意見頂戴できると嬉しいです:pray:

<!---
そして、Slack App でなにかするときに何かしら外部のサービスと連携する需要もあると思います。
Bot用のアクセストークンでやれるものなら、特に面倒はないですが、個々のアカウントによる操作であれば Personal Access Token か OAuth 認証を使ったを使う必要があります。
(例えば、弊社では Qiita Team を情報共有に使っていて、誰の投稿で誰のコメント、リアクションかとかが欲しいから Personal Access Token での操作が望ましい。とか)
-->

### やらない
- Slack App の作成とインストール
    - これは各所で説明があると思うので省きます
    - [Slack Appの作り方について - Qiita](https://qiita.com/yuukiw00w/items/94e4495fc593cfbda45c)
- デプロイ
    - いろんな環境へのデプロイが考えられるしどうしようか考え中
    - そもそもまだデプロイまでやってない

## Bolt、jsx-slackについて

### :zap: Bolt
Bolt は Slack App をスッと開発できるように作られたフレームワークです。
イベントをリスニングするエンドポイントを作ったり、APIのリクエストをしたりすることが簡単にできます。

:point_down: のコードですぐに立ち上がります。
[slackapi/bolt: A framework for building Slack apps](https://github.com/SlackAPI/bolt#initialization)

本来であれば、Webアプリケーションサーバをたててイベントを受け付けるエンドポイントをつくり、リクエストのボディを処理して、何らかのロジックを組み込む必要がありますが、Boltを使えば Slack App のロジック部分を作ることに集中できます。

### jsx-slack
まず、jsx-slack の前に[Block Kit](https://api.slack.com/block-kit) についてです。

#### Block Kit

Block Kit は Slack App のための UI フレームワークです。
メッセージやモーダル、最近では Slack App の Home タブで UI コンポーネントを組み込み、インタラクティブな操作が可能になっています。

- 画像
- フォーム
- テキスト

などをかなり自由にレイアウトすることができます。

本来であれば、 [Block Kit Builder](https://api.slack.com/tools/block-kit-builder)を使って GUI でつくって生成される JSON をコードに組み込むことになりますが、一度コードに組み込んだあとのメンテナンスが辛くなります。

---------------

それを解決してくれるのが jsx-slack です。
名前のとおり、JSX で Block Kit で作る UI の記述ができます。

:point_down: 下記サンプルのように JSX だとコードをみて直感的に UI のイメージがつきやすくなります。
[speee/jsx-slack: Build JSON object for Slack Block Kit surfaces from readable JSX](https://github.com/speee/jsx-slack#jsx-slack)


## とりあえず動かすところまで
### ボイラープレート + Sample Code
Bolt + TypeScript + jsx-slack でボイラープレート作ったのでこちらを使って動かすとこまで行くことを想定しています。見ながら参考にしてください。
[phigasui/slack_app_boilerplate](https://github.com/phigasui/slack_app_boilerplate)

これでいつでもサクッと Slack App がつくれるぞ :muscle: 

TypeScriptをトランスパイルせずそのまま実行できるように [ts-node](https://github.com/TypeStrong/ts-node) をつかいます。
また、開発中はホットリロードしてほしいので [nodemon](https://github.com/remy/nodemon#nodemon) をつかいます。

### Slack App 作って Token を設定

PORT 番号はよしなに

```.env
SLACK_SIGNING_SECRET=****************************
SLACK_BOT_TOKEN=xoxb-**********************************

PORT=3000
```

### ローカルで動かす

Bolt のチュートリアルでも使っている [ngrok](https://ngrok.com/) をつかってローカルで Slack App を使えるようにします。

各々の環境で ngrok をインストールし、

```shell
$ ngrok http {PORT}
```

再実行すると、Slack App に設定する URL を変えないとならないため、かなりしんどい

### Slack App に エンドポイントのURLを設定
Bolt で作るアプリケーションは `/slack/events` をリスニングするため、`ngrok` が発行した URL を使って下記のように設定します。


#### スラッシュコマンド
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32147/ec737360-314e-c8b0-cb34-a58121340da4.png)

#### モーダルなどでのアクション
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32147/b9f10fbb-ffa5-8ba5-eded-db2465b94351.png)


### Slack App を立ち上げる

```
$ yarn start
  yarn run v1.19.1                                                                                                                                                   
  $ ./node_modules/.bin/nodemon                                                                                                                                      
  [nodemon] 2.0.1                                                                                                                                                    
  [nodemon] to restart at any time, enter `rs`                                                                                                                       
  [nodemon] watching dir(s): src/**/*                                                                                                                                
  [nodemon] watching extensions: ts,tsx                                                                                                                              
  [nodemon] starting `./node_modules/.bin/ts-node ./src/App.ts`                                                                                                      
  ⚡️ Bolt app is running!
```


## 実装について

### ディレクトリ構成
JSX のコードはメインではなく、ロジックのコードが `.tsx` になってしまうのは嫌だったので `Views.ts` で各種 `.tsx` をimport して一括でexport して切り離すようにしました。

```sh
$ tree src
src
├── App.ts
├── Views.ts
└── views
    └── SampleFormModal.tsx
```

### スラッシュコマンドの定義とBoltの実行
```ts
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET
})

app.command('/sample_form_modal', async ({ ack, body, context, payload }) => {
  ack()

  const user_name: string = body.user_name

  try {
    app.client.views.open({
      token: context.botToken,
      trigger_id: payload.trigger_id,
      view: SampleFormModal({ name: user_name })
    })
  } catch (error) {
    console.log(error)
  }
})

const run = async () => {
  await app.start(process.env.PORT || 3000)

  console.log('⚡️ Bolt app is running!')
}

run()
```

### View を作る
JSX で Block Kit の UI をつくってそれを Block Kit Builder で表示できるツールを用意してくれていたため、それを利用しました。
[@speee-js/jsx-slack: Build JSON for Slack Block Kit from JSX](https://speee-jsx-slack.netlify.com/#modal)

```jsx
/** @jsx JSXSlack.h */
import JSXSlack, { Input, Modal, Section, Textarea } from '@speee-js/jsx-slack'

export default ({ name }: { name: string }) => {
  return JSXSlack(
    <Modal title="Article" close="Cancel" callbackId="send_form">
      <Section>
        Hello {name}!
        <p>記事を作成します。</p>
      </Section>

      <Input label="タイトル" type="text" blockId="title" actionId="title" required />
      <Input label="タグ" type="text" blockId="tags" actionId="tags" placeholder="スペース区切りで複数登録 ex.'Ruby Rails'" />
      <Textarea label="本文" blockId="body" actionId="body" required />

      <Input type="submit" value="Save" />
    </Modal>
  )
}
```

サンプルを見ながら作ってみるとそれっぽくできるため JSX のありがたみを感じます。

サンプルのフォームを見るとなんとなくわかるのですが、もともと実装していたものでは Qiita、Qitta Team の Personal Access Token の保存のための DB アクセスや API リクエストを組み込んでいますが、今回は割愛:pray:

### モーダルからのアクションの受け取り

上記 view で作ったフォームを送信するとイベントが送信されるためそれをリスニングする `app.view` を定義します。

```ts
interface FormViewStateValues {
  values: {
    title: { title: { value: string } }
    tags: { tags: { value: string } }
    body: { body: { value: string } }
  }
}

app.view('send_form', async ({ ack, body, context, view }) => {
  ack()

  const user_id: string = body.user.id

  const form_view_state_values = (view.state as FormViewStateValues).values
  const form_title = form_view_state_values.title.title.value
  const form_tags = form_view_state_values.tags.tags.value
  const form_body = form_view_state_values.body.body.value

  app.client.chat.postMessage({
    token: context.botToken,
    channel: user_id,
    text: `Title: ${form_title}\nTags: ${form_tags}\nBody: ${form_body}`
  })
})
```

`ViewOutput.state` は作ったUIで変わるため Bolt では抽象度の高い Object になってしまいます。
そのため、TSで扱うには自分でどんな Interface になるか定義する必要があります。

[bolt/index.ts at 5ed8c89f3442448f7b0c1c2c8581d46b93533e7b · slackapi/bolt](https://github.com/slackapi/bolt/blob/5ed8c89f3442448f7b0c1c2c8581d46b93533e7b/src/types/view/index.ts#L72)

ここはしんどいですが、`ViewOutput` や `SlackViewAction` など、コールバックで受け取る引数のオブジェクトがどんなプロパティを持っているのか補完が効くところで TypeScript で実装している恩恵が受けれて良いかなと思います。

## もうおしまい

Bolt や jsx-slack のおかげで Slack App を作る敷居がかなり下がっていて大変ありがたさを感じます:pray:
今後開発していく上で手放せない存在になりそうです。


## 参考
- [Slack Block Kit のメッセージをよりメンテナブルにする jsx-slack のご紹介 - Speee DEVELOPER BLOG](https://tech.speee.jp/entry/2019/04/23/094015)
- [実践 jsx-slack: jsx-slack + Bolt で Slack のモーダルを自在に操ろう - Speee DEVELOPER BLOG](https://tech.speee.jp/entry/2019/10/16/100022)
- [Slack | Bolt](https://slack.dev/bolt/ja-jp/tutorial/getting-started)
- [Block Kit | Slack](https://api.slack.com/block-kit)