Cloudflare Workers + D1を使ってSlackのスラッシュコマンドを作ってみた


Cloudflare R2を触った後はD1を触ってみようと思い、
Honoの練習やSlackのSlash Commandの練習を兼ねて遊んでみました。

# Cloudflare D1とは


https://blog.cloudflare.com/ja-jp/introducing-d1-ja-jp/

https://developers.cloudflare.com/d1/

詳しくは上記を読んでいただくと良いと思いますが、
ざっくり言うとCDNのエッジ上で動作するsqliteです。（本日時点でまだOpen Alpha）

と、自分などが説明するよりも、日本語でD1についてざっくり理解するなら以下を読んでいただくのが手っ取り早いかと思います。

https://zenn.dev/mizchi/articles/cloudflare-d1

https://zenn.dev/yusukebe/articles/1c3a66857eee37

自分は普段はMySQLがメインですが、エッジ側で sqlite がどこでも使えるというのは個人的にもかなりインパクトがありました。
ちょっとした用途だったらDBも含めて全部サーバレスで済んでしまうというのは本当に素敵だしヤバいです（語彙）。

# 何を作ったのか？
先週、ちょうどタイミング良くというか、社内勉強会的のような場所においてファシリテーター（司会・進行・質問等を促す人）をサクッとランダムに指名出来るような事が出来たら良いなという話が出てきました。

こういう場合はちょっとしたGAS(Google Apps Script)などでちょろっとやったりするのがお手軽で良いのかもですが、
せっかくなのでHonoの練習やD1に触れてみたいということで、これをSlackのスラッシュコマンドから出来ないかということで試してみました。

`/facilitator random`

みたいにSlackのSlash Commandを打つと、参加者の中からランダムに指名してメンションを送るような感じです。

せっかくなのでデータもD1に突っ込んでしまって、
ユーザの登録/削除やSlackユーザーIDの設定なども全部Slackコマンドから行けると良いのではと考えました。
![スクリーンショット 2023-07-31 2.06.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/8b71fd5f-f611-01ea-d879-a94fa1136bb6.png)

# サンプルを見ながら実装してみる

以前発表したR2の実装は公式のexampleをカスタマイズしたものであり、
実はHonoを使うのも今回がほぼ初めてなので、

https://zenn.dev/yusukebe/articles/1c3a66857eee37

https://github.com/yusukebe/hello-d1/tree/main

あたりを真似しつつ、公式のドキュメント

https://developers.cloudflare.com/d1/platform/client-api/

を写経しながらD1のCRUDを試していきました。

---

## 苦労した点

自分はTypeScriptどころかJavaScriptもあまり書いたことが無いので、ChatGPT先生に頻繁に聞きながら組み込んでいきました。

### SlackコマンドはREST APIでは無かった
Honoを使うことのメリットとして、Routingを作ることが非常に簡単な事だと思うのですが、
当初、Slackコマンドにする前はシンプルなREST APIみたいなのをイメージしていたので、

```
app.get(
    '/facilitator/list',
...

app.post(
    '/facilitator/add',
...

app.post(
    '/facilitator/update',
...
```

みたいに作っていたのですが、
SlackコマンドはRESTとは違ってエンドポイントに指定出来るURLが一つしかない事に気付きました…

![スクリーンショット 2023-07-31 2.55.44_mozaik.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/e5ce9f71-eb2e-b95c-0ff4-3e54f355ef78.png)

仕方ないので、めちゃくちゃ可読性が悪くなってしまいますが、
力技のSwitch文で無理やり分岐させました。。

```typescript
        switch (command) {
            case 'list':
                console.log('GET list');
                const { results } = await c.env.DB.prepare(
                    "SELECT username FROM users"
                )
                    .all();
                const usernames = results.map(function (obj) {
                    return obj.username;
                });
                return c.text(usernames.join('\n'));

                break;

            case 'dump':
```


### Slackコマンドから渡されたペイロードの取り方が分からなかった
これは公式のヘルプ

https://api.slack.com/interactivity/slash-commands

をちゃんと読まなかったのが悪いのですが、スラッシュコマンドの後ろのパラメータの取り方が分からずハマりました。
`console.dir(c.req)` や `warn Dumper(c.req)` みたいな事が出来ないので（出来るんでしょうか？）
いろいろ試して結局 `application/x-www-form-urlencoded` だったので、

https://hono.dev/api/request#parsebody

を見て

```typescript
        const body = await c.req.parseBody();
        const text = body.text;
        const [command, ...args] = text.split(' ');
```

のようにして取れたのですが、果たしてこれでいいのかどうか、
普段のAPIなどではあまりペイロード自体を取得する事を意識してなかったのでハマりました。。

### Only visible to you 問題
Slashコマンド自体を作るのも初めてだったので、
最初、

```typescript
return c.text('<@${randomUser.slack_user_id}> test');
```

みたいにするだけで特定のChannelにメンションする事が出来るのかなと思っていたのですが、
「Only visible to you」ということでチャンネルの中で打っても自分自身しか見れないことが判明ｗ

結局Slack APIを使うしか無いのか、、ということで探したところ

https://www.npmjs.com/package/@sagi.io/workers-slack

を見つけたのですが、 `node_compat` の組み込み方が分からず時間も無かったので結局素のfetchでPOSTを投げることにｗ

```typescript
const response = await fetch('https://slack.com/api/chat.postMessage', {
    method: 'POST',
    headers: {
        'Authorization': `Bearer ${slackApiToken}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        'text': message,
        'channel': channelName
    })
});
```

# とりあえず動いた

ハマりつつも、とりあえず最低限が動いたので以下にコミットしました。

https://github.com/teckl/cf-workers-d1-slack-facilitator/tree/main

こんな感じに動きます。

- Usage
![スクリーンショット 2023-07-31 2.06.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/1af8693a-67d6-b8eb-9589-11dd6d289130.png)

- /facilitator list
![スクリーンショット 2023-07-31 2.05.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/f88c11f6-df52-4d82-0709-d029362f0fee.png)

- /facilitator random
![スクリーンショット 2023-07-31 2.05.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/83732f38-6cc6-cb15-0531-05c616cde2ea.png)

- /facilitator dump
![スクリーンショット 2023-07-31 2.07.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/28477/a0836a0a-e0cc-bb2d-15bb-151ba208a87f.png)

# TODO
なお、あくまでも社内の遊び用なので現状では認証やバリデーションなどは組み込んでいません。

こちらについてはまた後日試してみようと考えています。


# まとめ
- Hono はやっぱり使いやすかった
    - しかし今回のSlackコマンドだと本来の力をうまく引き出せなかった
- Cloudflare D1 思ったよりも使えそう
  - ちょっとしたAPIやサイトなら既に大体の事が出来るのでは？
      - トランザクションは現状無いが、必要無いような用途であれば
  - 早くGAして欲しい
  - コストも本気で期待
- SlackのSlash Commandsは思ったよりも異世界だった
    - 初物を欲張って複数同時にやるべきではなかった…
- 初めての言語等で分からない事があってもChatGPT先生に相談すればだいたいなんとかなりそう

# D1をもっと実用的に
ORMだったり、D1をもっと実践的なアプリケーションのコードを書きたいのであれば、以下の記事がとても参考になりそうです。

https://zenn.dev/chimame/articles/60e2824a42fec0

本件を作り終わってから気づいたのですが、本格的なSlackアプリを作りたいなら以下のようなライブラリがあるようです。これを使えばだいぶ楽が出来ていたのかも…

https://zenn.dev/seratch/articles/c370cf8de7f9f5

# その他参考
https://dev.classmethod.jp/articles/lets-make-slack-commands-synchronous-execution-version/

https://dev.classmethod.jp/articles/notify-daily-scrum-facilitator-slack-bot/

https://dev.classmethod.jp/articles/getting-started-cloudflare-workers-hono-cloudflare-d1/

https://zenn.dev/razokulover/articles/4d0ba10083524e