DiscordenoでDiscord Botを作ってみる

## はじめに

完全無料で常時稼働できる Discord Bot の公開先として Deno を試してみたので備忘録を兼ねて記事にまとめてみました。

:::note warn
本記事の内容は2025年2月時点での情報であることに注意してください。
:::


### Denoとは
[Deno](https://deno.com/)は、JavaScript および TypeScript の実行環境であり、Node.js の開発者である [Ryan Dahl](https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%BB%E3%83%80%E3%83%BC%E3%83%AB)によって開発されました。Deno は、Node.js における[10個の設計ミス](https://yosuke-furukawa.hatenablog.com/entry/2018/06/07/080335)を解決することを目的として開発されました。代表的なものとして以下のような特徴が挙げられます。
- セキュリティ強化：デフォルトでファイル・ネットワーク・環境変数へのアクセスを制限
- TypeScript の標準サポート：追加の設定なしで TypeScript を直接実行可能
- ES モジュールの採用：import を使用し、URL から直接モジュールを取得できる

### 本記事の構成
本記事では、Discordeno を用いて Discord Bot を開発し、デプロイするまでの手順を解説します。
[1. Discord Botの準備](https://qiita.com/rentaro079/items/fffe8b8cc5855f54fe2e#1-discord-bot-%E3%81%AE%E6%BA%96%E5%82%99)
[2. Denoをインストールする](https://qiita.com/rentaro079/items/fffe8b8cc5855f54fe2e#2-deno-%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)
[3. 設定ファイルの用意](https://qiita.com/rentaro079/items/fffe8b8cc5855f54fe2e#3-%E8%A8%AD%E5%AE%9A%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E7%94%A8%E6%84%8F)
[4. オウム返しBot の実装](https://qiita.com/rentaro079/items/fffe8b8cc5855f54fe2e#4-%E3%82%AA%E3%82%A6%E3%83%A0%E8%BF%94%E3%81%97-bot)
[5. Denoにデプロイする](https://qiita.com/rentaro079/items/fffe8b8cc5855f54fe2e#5-deno-%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B)

### 開発環境

| 名前        | バージョン |
| ----------- | ---------- |
| Deno        | 2.1.9      |
| TypeScripts | 5.6.2      |
| Discordeno  | 20.0       |

## 実装

### 1. Discord Bot の準備

1. デベロッパーアカウントを作成するために以下のサイトにアクセスします

   [Discord Developer Portal](https://discord.com/developers/applications)

2. 右上の`New application`をクリックします
    ![img_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/0cc306c8-a183-4ea7-b6cf-bd382404862f.png)

3. Bot の名前を設定し`Create`をクリックします（`name`は後から変更可能）

   ![img_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/5a336a1f-64ae-4cf1-8320-843d297e2810.png)


4. `Bot`タブの`TOKEN`にある`Reset Token`をクリックするとトークンをコピーできるようになります

   ![img_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/565030e2-9235-4ec1-a3e8-d7323a338b37.png)


5. `Bot`タブの`Privileged Gateway Intents`の`SERVER MEMBERS INTENT`と`MESSAGE CONTENT INTENT`にチェックを入れます

    ![img_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/080507c9-5eaa-46ea-a8c0-6a928ddc6a8a.png)


   **`SERVER MEMBERS INTENT`**：Bot が**サーバーメンバーの情報（参加・退出・ニックネーム・ロール変更など）をリアルタイムで取得できる**ようになります。

   **`MESSAGE CONTENT INTENT`**：Bot が**サーバー内のメッセージの内容（テキスト）を読み取れる**ようになります。

6. `OAuth2`タブの`OAuth2 URL Generator`にある`Scopes`の`bot`チェックを入れることで参加させるための認証 URL を発行できます

   ![img_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/a673ef0e-f7ad-4839-ad05-196b72a4059b.png)


7. 発行した URL にアクセスすると管理者権限のあるサーバに追加できるようになります

   ![img_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/6182200f-b0c3-49bd-a915-7cfabab9693a.png)


8. 必要に応じたロールを設定します。一般ユーザと同様に Discord 上で設定するか、`OAuth2`タブの`Bot Permissions`から設定できます。

   ![img_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/134b5c02-c83b-4497-b1f7-28f659bd69fa.png)![img_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/ba4bc786-d248-4dce-868d-d1e65be89bd4.png)

### 2. Deno をインストールする

npm がインストールされているという前提で進めます。

<details><summary>npmのインストール確認方法</summary>

```powershell
npm -v
```

</details>

PowerShell を管理者権限で開き以下のコマンドを実行して Deno をインストールします。

```powershell
iwr https://deno.land/install.ps1 -useb | iex
```

以下のコマンドでインストールできているか確認します。

```powershell
deno --version

# deno 2.1.9 (stable, release, x86_64-pc-windows-msvc)
# v8 13.0.245.12-rusty
# typescript 5.6.2
```

これで Deno のインストールは完了です。

### 3. 設定ファイルの用意

1. `deno.jsonc`というファイルを作成します

   ```deno.jsonc
   {
     "lock": false, //deno.lock（モジュールのバージョン管理ファイル）を生成しないようにする
     "tasks": {
       "start": "deno run -A --watch ./main.ts" // 実行するファイルパス
     },
     "imports": {
       "$std/": "https://deno.land/std@0.224.0/",
       "@discordeno/": "https://deno.land/x/discordeno@20.0.0/" // 任意のバージョン
     }
   }
   ```

2. `.evn`というファイルを作成し、Discord Developer Portal の`Bot`タブに記載されている`TOKEN`をコピーして貼り付けます

   ```.env
   TOKEN=Th1si5d1sc0dTok3N
   ```

### 4. オウム返し Bot

ユーザのメッセージをそのまま返すBotを実装します。

```main..ts
import {
  createBot,
  getBotIdFromToken,
  startBot,
  Intents,
} from "@discordeno/mod.ts";
import "$std/dotenv/load.ts";

// .env からトークンを取得
const token = Deno.env.get("TOKEN");

const bot = createBot({
  token,
  botId: BigInt(getBotIdFromToken(token)),
  intents: Intents.Guilds | Intents.GuildMessages | Intents.MessageContent,
  events: {
    // 起動時の処理
    ready: (_bot, payload) => {
      console.log(`✅ Bot is online!`);
    },

    // メッセージ受信時の処理
    messageCreate: async (bot, message) => {
      if (message.authorId === bot.id) return; // ボット自身のメッセージを無視
      await bot.helpers.sendMessage(message.channelId, {
        content: message.content,
      });
    },
  },
});

// ボットの常時起動
Deno.cron("Continuous Request", "*/3 * * * *", () => {
  console.log("🔄 Bot is active!");
});

// ボットを起動
await startBot(bot);
```

Deno Deploy では、5 分間リクエストがないとスリープ状態になる仕様があります。そこで`Deno.cron`を用いてログを出力し、Bot を寝かさないようにします。`Deno.cron` は、Deno のスケジュール実行機能で、指定した間隔で定期的に関数を実行するためのものです。今回のコードでは、ボットを 3 分ごとに起動してログを出力する仕組みになっています。

`*/3 * * * *`の部分が corn 式で、以下のような構造になっています。

| ＊     | ＊               | ＊     | ＊     | ＊                 |
| ------ | ---------------- | ------ | ------ | ------------------ |
| 分     | 時               | 日     | 月     | 曜日               |
| 0 - 59 | 0 - 23(UTC 時間) | 1 - 31 | 1 - 12 | 0 - 6(日曜 → 土曜) |

注意したいのが、UTC(協定世界時)を用いていることです。`UTC = JST - 9時間`と表現されます。現在は UTC に置き換わりつつありますが、GMT と同じ時間を示します。

Bot を起動するには以下のコマンドを実行します。

```powershell
deno task start
```

これでテキストチャンネルにメッセージを送信すると Bot が返信してくれます。

![img_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/05fb1ac4-78e3-4b30-a71f-fad0f0a08c49.png)


### 5. Deno にデプロイする

1. 作成したプログラムを GitHub リポジトリにプッシュしておきます。
   トークンなどを記載したファイルは.gitignore に追加しておきましょう。

2. [Deno Deploy](https://dash.deno.com/account/overview)にアクセスし、GitHub と連携する

   ![img_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/0983b9de-4bcb-4394-abc0-a35b70d4468f.png)


3. 右上の`New Project`をクリックします

   ![img_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/b98c5af9-91ac-4c9a-8e51-8bc35c8c024f.png)


4. 先ほどプッシュしたリポジトリを選択します

   ![img_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/9ff32b97-ffad-4ac2-a293-66460e27f582.png)


5. `Project Configuration`で`Ptoject Name`を設定し、`Entrypoint`に実行ファイル名を指定します。すべて設定できたら`Deploy Project`をクリックしてデプロイを開始します。

   ![img_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/ff84ad9b-c91a-4ecb-bacf-3b5cf74b1245.png)


6. トークンを`.env`などで管理しているとトークンを取得できずに以下のようにデプロイに失敗します。そのため Deno 側で環境変数を設定します。画面右上の`Settings`をクリックします。

   ![img_13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/a86d86df-3c7a-4f6a-b1d5-471b8618ff0b.png)


7. `Environment Variables`の`Add Variable`をクリックし`KEY`に環境変数名、`VALUE`に`.env`で設定していたトークンを入力します。`Save (1 new)`を押して設定完了です。

   ![img_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/d2137f4f-61cb-410a-b7d1-786e35242901.png)

   デプロイが成功すれば Bot が常時オンラインになります。

   <details><summary>うまくいかなかったら…</summary>

   ![img_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/b29b3020-fede-4c5f-9b19-4a201ad70f36.png)
   **Environment Variables**でトークンを設定した後も上記のようなエラーが吐かれるなら、手動で GitHub に空コミットをプッシュしてみると良いかもしれません。
    </details>

## 詰まったところ

- サーバのメンバーをオフラインメンバーも含めて取得

  `Bot`タブの`Privileged Gateway Intents`で設定できる`Server Members Intent`および`Presence Intent`が必要
  ![img_16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/e41f8928-d05a-4e23-a08a-cea068f75e25.png)


- アバター画像の取得

  ![img_17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/972e1336-1225-4d5b-93f9-dbd860708723.png)


  Discord のアバター画像の URL は以下のように管理されています。

  ```
  https://cdn.discordapp.com/avatars/userId/avatarHash.png
  ```

  `avatarHash`の部分は 16 進数で表現されています。しかし Discordeno の`member.user.avatar`で取得できるものは 10 進数で表現されていました。これを 16 進数に変換、かつ最初の一桁目を落とす必要がありました。

  ```js
  `https://cdn.discordapp.com/avatars/${userId}/${member.user.avatar.toString(16).slice(1)}.png`
  ```

- ユーザ－のメインプロフィールの表示名を取得

  左のユーザープロフィールの表示名（アドミン）は取得することができませんでした。右のサーバプロフィールのサーバニックネーム（Admin）は`member.nick`で取得でき、自分のアカウントのユーザ名（Adm1n_d4mmyUsern4me）は`member.user.username`で取得できました。
  ![img_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3828517/0d1973ba-b441-48dd-b9af-4ae531f21a51.png)

  `member`オブジェクト、`user`オブジェクトを見てみても`globalName`や`displayName`などは見つけられず、ユーザの表示名の取得はあきらめました。（どなたか取得方法を知っていたら教えてください；；）

  ```tsx
  member： {
    id: xxxx,
  	...,
    user: {
      id: xxxx,
      username: "Adm1n_d4mmyUsern4me",
      ...
    },
    nick: "Admin",
    roles: [],
    ...
  }
  ```

## おわりに

今回、Discordeno を使って Discord Bot を作成してみました。体感ですが公式ドキュメントがあまり充実しておらず、分かりづらかったです。また、バージョン間の互換性の問題で正常に動作しないケースも多かったように感じました。しかし、Heroku が有料化した今、無料で常時稼働できる点は Deno の大きな魅力だと思います。ここまで読んでいただきありがとうございました。

## 参考にしたサイト等

https://discordeno.js.org/docs/intro

https://qiita.com/23tas9/items/8141aa674f1f7d71f529

https://zenn.dev/platina/articles/b57817025d08de