SkeetサーバーレスフレームワークでDiscord Webhooksをマスター 🔧🌐

Skeet フレームワークを使用して Firebase Functions と Discord Webhooks の統合を探究します。このチャプターでは、Skeet のエコシステム内で Firebase Functions を活用して Discord イベントを処理する方法の詳細に迫ります。リアルタイムで反応するボット機能を作成し、ユーザーインタラクションを高めるための洞察を得ます。

## Skeet に Discord Interaction Webhook のエンドポイントを追加する

Discord Interaction Webhook のエンドポイントを追加することで、
Discord ボットの Interaction を受け取ることができます。

`skeet add webhook` コマンドを使って、
Discord Interaction Webhook のエンドポイントを追加しましょう。

```bash
skeet add webhook
? Select Webhook Type (Use arrow keys)
❯ discord
  stripe
✅ discordRouter added 🎉
✅ helloAction added 🎉
✅ helloCommand added 🎉
✅ helloIndex added 🎉
✅ deployCommands added 🎉
✅ helloMessage added 🎉
🔗 ./functions/skeet/src/routings/http/discordRouter.ts
🔗 ./functions/skeet/src/lib/discord/actions/helloAction.ts
🔗 ./functions/skeet/src/lib/discord/commands/hello.ts
🔗 ./functions/skeet/src/lib/discord/commands/index.ts
🔗 ./functions/skeet/src/lib/discord/deploy-commands.ts
🔗 ./functions/skeet/src/lib/discord/messages/helloMessage.ts
✔ Successfully exported to ./functions/skeet/src/index.ts 🎉
```

Discord Interaction Webhook のエンドポイント及び、
以下のツリーのように `discord` ディレクトリが作成され、
必要なファイルが追加されました。

```bash
tree -L 2 functions/skeet/src/lib
functions/skeet/src/lib
├── config.ts
├── discord
│   ├── actions
│   ├── commands
│   ├── deploy-commands.ts
│   └── messages
...
```

- actions - Discord インタラクションアクションの関数を定義するディレクトリ
- commands 　- Discord スラッシュコマンドの関数を定義するディレクトリ
- messages 　- Discord ボタンメッセージの関数を定義するディレクトリ
- deploy-commands.ts - Discord スラッシュコマンドをデプロイする関数ファイル

## アプリのデプロイ

変更したエンドポイントのみをデプロイするには、
`--function` オプションを使います。

```bash
skeet deploy --function skeet:discordRouter
```

カンマ区切りで複数のエンドポイントを指定することもできます。

```bash
skeet deploy --function skeet:discordRouter,skeet:root
```

## Discord インタラクション Webhook の登録

ここで、Discord Developer Portal に移動し、
Discord インタラクション Webhook を登録します。

`discordRouter` のエンドポイントを表示させるには、
以下のコマンドを実行します。

```bash
skeet get https
```

表示されたエンドポイントをコピーし、
Discord Developer Portal の Webhook URL に設定し保存します。

![](https://storage.googleapis.com/zenn-user-upload/652d8724cf63-20231114.png)

エンドポイントが無事に登録されると、
Discord Developer Portal の Webhook URL に設定したエンドポイントに
Discord ボットからの Interaction が送信されます。

エンドポイントに問題がある場合は、ここでエラーが起きます。

問題が起きた場合には以下のコマンドでエラーを確認し、
エンドポイントを修正してください。

```bash
skeet logs
```

## Discord ボタンメッセージの登録・実行

Discord ボタンメッセージのテンプレートは、
以下のディレクトリにあります。

`functions/skeet/src/lib/discord/messages/`

デフォルトでは `helloMessage.ts` が登録されています。

```typescript
import {
  ButtonStyleTypes,
  MessageComponentTypes,
} from '@skeet-framework/discord-utils'

export const helloMessage = () => {
  const body = {
    content: 'hello button',
    components: [
      {
        type: MessageComponentTypes.ACTION_ROW,
        components: [
          {
            type: MessageComponentTypes.BUTTON,
            style: ButtonStyleTypes.PRIMARY,
            label: 'Hello Button',
            custom_id: 'hello-button',
          },
        ],
      },
    ],
  }
  return body
}
```

この関数を `root` エンドポイントから呼び出して、
Discord ボタンメッセージを送信します。

`functions/skeet/src/routings/http/root.ts`

```typescript
import { onRequest } from 'firebase-functions/v2/https'
import { publicHttpOption } from '@/routings/options'
import { TypedRequestBody } from '@/types/http'
import { RootParams } from '@/types/http/rootParams'
import { helloMessage } from '@/lib/discord/messages/helloMessage'
import { messageChannel } from '@skeet-framework/discord-utils'
import { defineSecret } from 'firebase-functions/params'

const DISCORD_TOKEN = defineSecret('DISCORD_TOKEN')
export const root = onRequest(
  { ...publicHttpOption, secrets: [DISCORD_TOKEN] },
  async (req: TypedRequestBody<RootParams>, res) => {
    try {
      const message = helloMessage()
      const channelId = 'your-channel-id'
      await messageChannel(DISCORD_TOKEN.value(), channelId, message)
      res.json({ status: 'success' })
    } catch (error) {
      res.status(500).json({ status: 'error', message: String(error) })
    }
  }
)
```

`DISCORD_TOKEN` は、Secret Manager に登録した Discord のトークンが defineSecret で定義されています。

`channelId` は、お好きなチャンネルの ID を設定してください。

それでは `skeet s` コマンドでローカルサーバーを起動し、
ログに表示される `root` エンドポイントにアクセスしてみましょう。

```bash
skeet s
✔  functions: Loaded functions definitions from source: discordRouter, root.
✔  functions[asia-northeast1-discordRouter]: http function initialized (http://127.0.0.1:5001/your-project-id/asia-northeast1/discordRouter).
✔  functions[asia-northeast1-root]: http function initialized (http://127.0.0.1:5001/your-project-id/asia-northeast1/root).
```

`http://127.0.0.1:5001/your-project-id/asia-northeast1/root` にアクセスすると、
Discord ボタンメッセージが送信されます。

![](https://storage.googleapis.com/zenn-user-upload/84426634713a-20231114.png)

ボタンを押してみると

![](https://storage.googleapis.com/zenn-user-upload/5df85eb9cc92-20231115.png)

無事にレスポンスが返ってきました 🎉

## Discord スラッシュコマンドの登録・実行

Discord スラッシュコマンドのテンプレートは、
以下のディレクトリにあります。

`functions/skeet/src/lib/discord/commands/`

デフォルトでは `hello.ts` が登録されています。

```typescript
import { SlashCommandBuilder } from '@skeet-framework/discord-utils'

export const data = new SlashCommandBuilder()
  .setName('hello')
  .setDescription('Hello Slash Command')
  .addStringOption((option) =>
    option.setName('hey').setDescription('Say something').setRequired(true)
  )
```

この関数を `deploy-commands` エンドポイントから呼び出して、
Discord スラッシュコマンドをデプロイします。

`skeet deploy --discord` を実行すると、
デプロイ必要な `DISCODE_TOKEN` を Secret Manager から呼び出して実行することができます。

```bash
skeet deploy --discord
Started refreshing application (/) commands.
Successfully reloaded application (/) commands.
✨  Done in 4.01s.
```

無事にスラッシュコマンドがデプロイされました🎊

![](https://storage.googleapis.com/zenn-user-upload/0e79a9c99326-20231115.png)

スラッシュコマンドを実行してみると、

![](https://storage.googleapis.com/zenn-user-upload/b3e302ff2a16-20231115.png)

無事にレスポンスが返ってきました 🎉

## Discord インタラクションアクションの登録

Discord インタラクションアクションのテンプレートは、
以下のディレクトリにあります。

`functions/skeet/src/lib/discord/actions/`

デフォルトでは `helloAction.ts` が登録されています。

```typescript
import {
  DiscordRouterParams,
  deferResponse,
  updateResponse,
} from '@skeet-framework/discord-utils'
import { Response } from 'firebase-functions/v1'
import { inspect } from 'util'

export const helloAction = async (
  res: Response,
  db: FirebaseFirestore.Firestore,
  discordToken: string,
  body: DiscordRouterParams
) => {
  console.log('helloAction')
  console.log(inspect(body))
  await deferResponse(discordToken, body.id, body.token)
  // define your logic here
  await updateResponse(discordToken, body.application_id, body.token, {
    content: 'hello response!',
    flags: 64,
  })
  return true
}
```

ボタンアクションやスラッシュコマンドが実行されると、
Discord から `discordRouter` エンドポイントにリクエストが送信されます。

`discordRouter` エンドポイントは、
`functions/skeet/src/routings/http/discordRouter.ts` に定義されています。

関数の中の if 文で、
リクエストの種類に応じて、
`helloAction` や `helloCommand` などの関数を呼び出しています。
デフォルトでは、`helloAction` が `hello`コマンド、 `hello-button` トリガー発動時に呼び出されます。

```typescript
...
if (req.body.type === InteractionType.PING) {
  // To verify the request
  res.send({
    type: InteractionType.PING,
  })
} else if (req.body.type === InteractionType.APPLICATION_COMMAND) {
  // Slash command
  const options = req.body.data as CommandData
  if (options.name.match(/^hello$/)) {
    await helloAction(res, db, DISCORD_TOKEN.value(), req.body)
  }
} else {
  // Button action
  const { custom_id } = req.body.data as ActionData
  if (custom_id.match(/^hello-button$/)) {
    await helloAction(res, db, DISCORD_TOKEN.value(), req.body)
  }
}
...
```

それでは次の章で、これらの関数と Firestore を組み合わせて、
Discord アプリを作成していきましょう。