エンジニア歴２年目が初めてdiscordbotを作成してみた

# 自己紹介
AdventCalendar5日目の投稿をします。エンジニア歴2年目の吹春です。

これまではウェブアプリケーションをメインに個人開発をすることはあったのですが、書きっぱなしになることが多かったので、記録に残したいと思い参加しました。
今回は最近ハマっていて、毎日遊んでいる崩壊スターレイルに関するdiscordbotを作成したので作成過程を記事にします！

# 作成したbotの概要
- ローカルサーバーで動くdiscordbot
- スラッシュコマンドに反応して処理を実行
- 任意の引数に応じて、効果命中率を返却

# 事前準備
下記については本記事では説明していません。
- Node.jsのインストール
- discordアカウントの作成
- botを招待するdiscordサーバーの作成

# botアカウントのセットアップ
botアカウントを作成し、サーバーに招待します。

### 1. アカウント作成
[開発者サイト](https://discord.com/developers/applications)を開く。
「NewApplication」をクリック。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/bb656d29-7856-94fc-a16e-94cc052ed5ee.png" width=75%>

「NAME」を入力、「チェックボックス」にチェックを付け、「Create」をクリック。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/661cf5f4-e396-8302-e6c4-e474e12350de.png" width=40%>

### 2. プログラム作成時に必要な値のメモ
メニューの「General Information」を選択し、「APPLICATION ID」をコピーして控えておく。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/b05e092e-b372-28f3-518d-19d81e8f157f.png" width=75%> 

メニューの「Bot」を選択し、「TOKEN」をコピーして控えておく。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/230eccbb-3cdc-8c38-89dc-514b49f2b70a.png" width=75%> 

:::note info
botの詳細設定
TOKENは一度画面遷移するとコピーできなくなるが、「Reset Token」で再発行できます。
また、この画面でbotの詳細設定を行うことができますが、今回は不要のため省略します。
:::

### 3. botをサーバーに招待
メニュー「OAuth2」配下の「URL Generator」を選択し、「SCOPES」でbot、「BOT PERMISSIONS」でAdministratorをチェック。
生成された「GENERATED URL」でブラウザ検索する。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/37e31a54-2d52-83fa-be7b-46a9fe12eeb9.png" width=75%> 
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/5d1c0abc-0a05-8e89-931d-10a79bf7bb3f.png" width=75%> 

:::note info
BOT PERMISSIONS
botに付与する権限は、使用目的に応じて適切な選択をする必要があります。
今回は自サーバーのみで使用するため管理者権限を付与しています。
:::

検索後、botを招待したいサーバーを選択し、「はい」をクリック。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/182c9b46-c76e-7ec7-68c6-af8fd5a44b60.png" width=75%> 

「認証」をクリック。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/8abf0c4e-0597-520d-621e-21692b635ae9.png" width=75%> 

ブラウザの人間確認を実行したのち、下記が表示されれば招待成功です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/9d69d1f8-d210-398d-eecd-2be2a53cc022.png" width=75%> 

# botプログラムの作成
discord.jsを用いて、discordテキストチャンネルからのスラッシュコマンドに対応する処理を作成します。
VSCodeで開発するため、環境が異なる場合は適宜読み替えてください。

### 1. 環境準備
プログラムを記述する前準備をします。
#### 1.1. プロジェクトフォルダの作成
コマンドプロンプトでプロジェクトフォルダを作成したいディレクトリに移動したのち、下記を実行。
```markdown:コマンドプロンプト
mkdir myStarrailBot
```

#### 1.2. npm初期化
VSCodeでプロジェクトフォルダを開き、ターミナルでnpm初期化を実行。
```markdown:ターミナル
npm init -y
```
:::note info
npm初期化
プロジェクト管理ファイル（package.json）を作成することです。
:::

#### 1.3. discord.jsのインストール
プロジェクトフォルダにdiscord.jsをインストールする。
```markdown:ターミナル
npm install discord.js
```

#### 1.4. discordサーバーIDの取得
discordをデスクトップアプリまたはウェブアプリのどちらかで開き、左下の「歯車マーク」からユーザー設定を開く。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/f4f9656b-3f56-1601-7294-155c626e1a6a.png" width=75%> 

メニューから「詳細設定」を選択し、「開発者モード」をオンにする。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/e8cd8212-0f1b-5174-2011-392e976b3da8.png" width=75%> 

ユーザー設定を閉じ、botを導入したサーバーのアイコンを右クリック。
「サーバーIDをコピー」を押してサーバーIDを取得する。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/fe9ba587-70cc-cfcc-1769-0960c8be4b0d.png" width=75%> 

#### 1.5. 環境変数の設定
config.jsonファイルを作成し、
botアカウントのセットアップ時に控えた「APPLICATION ID」と「TOKEN」、discordサーバーIDの取得で取得した「サーバーID」をそれぞれ変数に設定する。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/1bf02d7b-ed5f-f28f-ecf5-99fd1c3a350a.png" width=75%> 

### 2. プログラムの作成
実際にbotを動かすためのプログラムを書いていきます。

#### 2.1. メイン(実行)ファイルの作成
プロジェクトフォルダ直下に「index.js」ファイルを作成し、bot起動のコードを記述する。
```javascript:index.js
// discord.jsライブラリから必要な部品をインポートする
const { Client, Events, GatewayIntentBits } = require('discord.js');

// 設定ファイルからTOKENを呼び出し、変数に格納する
const { token } = require('./config.json');

// クライアントインスタンスを作成する
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// クライアントインスタンスのログインが完了したとき、一度だけ実行される
client.once(Events.ClientReady, c => {
	console.log(`準備OKです! ${c.user.tag}がログインします。`);
});

// // クライアントインスタンスのイベントに対応する処理（イベントに対応する処理は後ほど記述）
// client.on("登録したいイベント", (hensuu) => {
// 	  // 対応する処理
// });

// ログイン
client.login(token);
```

:::note info
bot起動の確認

上記ファイルを作成すると、コマンド実行でbotの起動を確認することができます。
```markdown:ターミナル
node deploy-commands.js
```
botがオンライン状態になる。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/37876c29-ffe1-beca-37c3-2aeef073e0ad.png" width=75%> 
:::

#### 2.2. スラッシュコマンドファイルの作成
[discord.js公式ガイド](https://discordjs.guide/creating-your-bot/slash-commands.html#individual-command-files)を参考に、プロジェクトフォルダ直下に「commands」フォルダを作成し、その中に「effectHitRate.js」というファイルを作成し、中身を記述。

```javascript:commands/effectHitRate.js
// discord.jsライブラリから必要な部品をインポートする
const { SlashCommandBuilder } = require('discord.js');

// 処理で使用する定数を定義する
const effectRESList =[0, 10, 20, 30, 40];

// スラッシュコマンドに必要な情報をモジュール化する
module.exports = {
	data: new SlashCommandBuilder()
		.setName('ehr')
        .setDescription('基礎命中率と効果命中から命中率を計算します。')
        .addNumberOption(option =>
            option
              .setName('basechance')
              .setDescription('技の基礎命中率を入力してください。(%)')
              .setRequired(true)
        )
        .addNumberOption(option =>
            option
              .setName('effecthit')
              .setDescription('キャラクターの効果命中を入力してください。(%)')
              .setRequired(true)
        )
        .addNumberOption(option =>
          option
            .setName('debuffres')
            .setDescription('敵キャラクターの固有状態抵抗値を考慮する場合は入力してください。(%)')
            .setRequired(false)
        ),
	async execute(interaction) {
        var returnStr = '';
        const basechance = interaction.options.getNumber('basechance');
        const effecthit = interaction.options.getNumber('effecthit');
        const debuffres = (interaction.options.getNumber('debuffres') == null) ? 0 : interaction.options.getNumber('debuffres');
    
        for (const effectRES of effectRESList) {
          const result = (basechance / 100) * (1 + effecthit / 100) * (1 - effectRES / 100) * (1 - debuffres / 100) * 100;
          const resultStr = (result >= 100) ? '100%' : (result <= 0) ? '0%' : Math.round(result * 10) / 10 + '%';
          returnStr += '効果抵抗' + effectRES + '%の場合: ' + resultStr + '\n';
        }
    	await interaction.reply(returnStr);
	},
};
```

#### 2.3. メインファイルの編集

メインファイルに下記プログラムを追記する。
- 「effectHitRate.js」で作成したモジュールの読み込み
- スラッシュコマンドに反応するイベントリスナの登録

```javascript:index.js
// discord.jsライブラリから必要な部品をインポートする
const { Client, Events, GatewayIntentBits } = require('discord.js');

// effectHitRate.jsのモジュールを読み込む
const ehrFile = require('./commands/effectHitRate.js');

// 設定ファイルからTOKENを呼び出し、変数に格納する
const { token } = require('./config.json');

// クライアントインスタンスを作成する
const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// クライアントインスタンスのログインが完了したとき、一度だけ実行される
client.once(Events.ClientReady, c => {
	console.log(`準備OKです! ${c.user.tag}がログインします。`);
});

// スラッシュコマンドに反応するイベントリスナの登録
client.on(Events.InteractionCreate, async interaction => {
	// スラッシュコマンド以外には反応しない
	if (!interaction.isChatInputCommand()) return;

	// スラッシュコマンドの名前が一致したら処理を実行する
	if (interaction.commandName == ehrFile.data.name) {
		try {
			await ehrFile.execute(interaction);
		} catch (error) {
			console.error(error);
			await interaction.reply({ content: 'エラーが発生しました。', ephemeral: true });
		}
	}
});

// ログイン
client.login(token);
```

#### 2.4. コマンド登録ファイルの作成
サーバーにコマンドを登録するための処理を記述します。

```javascript:deploy-commands.js
// discord.jsライブラリから必要な部品をインポートする
const { REST, Routes } = require('discord.js');

// effectHitRate.jsのモジュールを読み込む
const effectHitRate = require('./commands/effectHitRate.js');

// トークンを読み込み、変数に格納する
const { applicationId, guildId, token } = require('./config.json');

// コマンドを配列に格納する
const commands = [effectHitRate.data.toJSON()];

// RESTクライアントを作成する
const rest = new REST({ version: '10' }).setToken(token);

// サーバーにコマンドを登録する
(async () => {
    try {
        await rest.put(
			Routes.applicationGuildCommands(applicationId, guildId),
			{ body: commands },
		);
        console.log('サーバー固有のコマンドが登録されました！');
    } catch (error) {
        console.error('コマンドの登録中にエラーが発生しました:', error);
    }
})();
```

:::note info
コマンドファイル複数作成時
コマンドファイルが複数ある場合は[discord.js公式ガイド](https://discordjs.guide/creating-your-bot/command-deployment.html#guild-commands)参照。
:::

# スラッシュコマンドの実行
作成したスラッシュコマンドを試してみる。

### 1. コマンドの登録
下記コマンドを実行し、discordサーバーにコマンドを登録する。
（ [2.1. メイン(実行)ファイルの作成](###21メイン(実行)ファイルの作成)でbotを起動している場合は「ctrl+c」で処理を中断し、再起動する。 ）
```markdown:ターミナル
node deploy-commands.js
```

### 2. コマンドの実行
`node index.js`でbotを起動し、discordのテキストチャットでスラッシュコマンド`/ehr`を実行する。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/37c22b02-adf4-1963-a8bc-3c8deb8e16fb.png" width=75%> 

実行結果がテキストチャットに返却される。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3553958/c4b00dc4-46c0-9145-1a02-a4a1e0d7d0f4.png" width=75%> 

# あとがき
記事書いてみた感想。

これまでの個人開発では、作ったことに満足して中途半端に終わることが多かったです。
しかし、今回記事にしてみて「discord.js」について理解が進まないまま作成した箇所や、作成したbotについてもっと改善したいなと思うことがたくさん出てきました。
**見直す機会があると、作ったプログラムに対してもっと良くしたい欲が湧いてくる**のでとてもいいなと思いました。
作成したbotについて、今後は崩壊スターレイルのapiを使用したりして作れたら楽しいだろうなと思っているので、いつか作ってまた記事にしたい、、、です！
今回の続きを作成するときは、簡単にまとめて記事を書こうと思います（未定）。

拙い記事だったと思いますが、ここまで読んでくださってありがとうございました！