ひぃの雑多書き

日記からお役立ち情報まで色んなことを書きます。

【v14】discordjsのすゝめ 第3回「スラッシュコマンドの実装」

こんにちは。ひぃです。

discordjsもある程度理解できたということで書き始めた「discord botの作り方」の第3回です。
今回はスラッシュコマンドを作ります。

◇環境
node: 16.13.2
discordjs: 14.7.1

3.スラッシュコマンドの実装

今回目指す成果

今回はこんな感じのスラッシュコマンドを作ります!

ファイル作成

こんな感じでフォルダとファイルを作りましょう!
中身は次から解説しつつ記載していきます。

ファイル名、フォルダ名の細かい違いに注意!

index.js

index.jsにも更新が入ります。

// discord
const Discord = require("discord.js");
const { Client, GatewayIntentBits, Partials } = require("discord.js");
const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildEmojisAndStickers,
  GatewayIntentBits.GuildIntegrations, GatewayIntentBits.GuildVoiceStates,
  GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessageReactions,
  GatewayIntentBits.DirectMessages, GatewayIntentBits.DirectMessageReactions, GatewayIntentBits.MessageContent],

  partials: [Partials.User, Partials.Channel, Partials.GuildMember, Partials.Message, Partials.Reaction]
});

// お手製
const app_root = require("app-root-path");
const config = require(app_root + "/config.js");
const load_commands = require(app_root + "/startup/load_commands.js");

/** 
 * スラッシュコマンド登録処理
 */
const deploy_commands = require(app_root + "/startup/deploy_commands.js");
deploy_commands.start();

/** 
 * インタラクション(コマンド)受信時の処理
 */
const interaction_create_handler = require(app_root + "/startup/interaction_create_handler.js");
interaction_create_handler.call(client);

// bot起動時の処理
client.on("ready", () => {
  console.log("ready!");
  client.user.setPresence({ activities: [{ name: `Blue Archive` }], status: "online" });
  client.channels.cache.get(config.channels.log).send("うん、たまにはこういうのも悪くない");
  
  // スラッシュコマンドの読込
  load_commands.start(client);
});


// bot起動処理
(async () => {
  try {
    // Discordログイン
    client.login(config.token);
  } catch (error) {
    console.log(error);
  }
})();

(18) スラッシュコマンド登録処理

deploy_commands.jsを呼び出してる所です。

詳細は後述しますが、commandsフォルダ配下を検索してコマンドを登録する処理になります。

(23) インタラクション(コマンド)受信時の処理

interaction_create_handler.jsを呼び出してる所です。 詳細は後述しますが、スラッシュコマンド起動時(インタラクション受信時)にコマンドの処理を実行する処理です。

(32) スラッシュコマンドの読込

load_commands.jsを呼び出してる所です。 登録されているコマンドを実際に使えるように読み込む処理です。

startup / deploy_commands.js

新規ファイルです。
commandsフォルダ配下を検索してコマンドを登録する処理です。

// お手製
const app_root = require("app-root-path");
const config = require(app_root + "/config.js");

// discord
const { REST, Routes } = require('discord.js');
const rest = new REST({ version: '10' }).setToken(config.token);

// node file system
const fs = require('node:fs');

// コマンド読み込み用
const commands = [];
const commandFolders = fs.readdirSync("./commands");

  
/**
 * スラッシュコマンド登録処理
 */
exports.start = async function() {
  // commandsフォルダの中のサブフォルダを読み込む
  for (const folder of commandFolders) {
    const commandFiles = fs.readdirSync(`./commands/${folder}`).filter(file => file.endsWith(".js"));

    // サブフォルダの中から1ファイルずつ処理する
    for (const file of commandFiles) {
      const command = require(`../commands/${folder}/${file}`);
      commands.push(command.data.toJSON());
    }
  }

  // コマンド登録処理
  (async () => {
    try {
      console.log(`${commands.length}個のコマンドの登録を開始します。`);

      // 全サーバで使えるコマンド登録
      const data = await rest.put(
        Routes.applicationCommands(config.clientId),
        { body: commands },
      );

      console.log(`${data.length} 個のコマンドを登録しました。`);
    } catch (error) {
      console.error(error);
    }
  })();
}

正直この処理はプログラミングが好きになってきてから理解するで良いと思います。
結構コメントは入れるようにしてるので、割とわかりやすいんじゃないかと。
読む気が起きないうちは「commandsフォルダの中身全部登録してるんだな~」くらいで大丈夫です。

ちゃんと知りたい人はこの辺。
Registering slash commands | discord.js Guide

startup / interaction_create_handler.js

新規ファイルです。
スラッシュコマンド起動時(インタラクション受信時)にコマンドの処理を実行する処理です。

botから見て)自分自身のコマンドかどうかを判断して、自分自身のコマンドであればその処理(後述するコマンドファイルのexecute部分)を呼び出しています。

const Discord = require("discord.js");

/**
 * インタラクション受信時の処理
 */
exports.call = async function(client) {
  client.on("interactionCreate", async interaction => {
    
    // コマンド実行時
    if (interaction.type === Discord.InteractionType.ApplicationCommand) {
      
      // 指定されたコマンドが見つからなければ中断
      const command = client.commands.get(interaction.commandName);
      if (!command) return;

      // コマンドを実行する
      await command.execute(interaction, client);
    }
  });
}

startup / load_commands.js

新規ファイルです。
登録されているコマンドを実際に使えるように読み込む処理です。

登録と読み込みって何がちゃうねん!って感じがするかもしれませんが、まぁ必要な処理なんだな~くらいに思っておいてください。 詳しく知りたかったらこの辺。

Command handling | discord.js Guide

const fs = require("fs");
const Discord = require("discord.js");

const commandFolders = fs.readdirSync("./commands");

/**
 * スラッシュコマンド読み込み処理
 */
exports.start = async function(client) {
  client.commands = new Discord.Collection();
  console.log("スラッシュコマンドの読み込み処理を開始します。")

  // commandsフォルダの中のサブフォルダを読み込む
  for (const folder of commandFolders) {
    const commandFiles = fs.readdirSync(`./commands/${folder}`).filter(file => file.endsWith(".js"));
    console.log(`===== ${folder} =====`);

    // サブフォルダの中から1ファイルずつ処理する
    for (const file of commandFiles) {
      const command = require(`../commands/${folder}/${file}`);

      try {
        // コマンド登録処理
        await client.commands.set(command.data.name, command);
        console.log(`${command.data.name}が読み込まれました。`)
      } catch (error) {
        console.log(error);
      }
    }
  }

  // 処理完了
  console.log("---------------")
  console.log("スラッシュコマンドの読み込み処理が完了しました。")
}

strings / string.js

bot内で使う文言を記載しています。

最初の内は「え?コマンドのファイルに直接書くんじゃダメなんか?」って思うかもしれませんが、後々特定のリアクションに対するハンドラを作成したり、一度作成したEmbedを編集しようと思うと細かい文言の違いが原因でエラー吐くと厄介なので、マジで外だししておいた方がいいです。

/** 
 * コマンド名、説明文
 */
exports.PING = "ping";
exports.PING_DESCRIPTION = "botが正常に動作しているかをチェックします。";

/** 
 * EMBED
 */
exports.EMBED_TITLE_SUCCESS = "Success!!";

/** 
 * その他
 */
exports.ZERO_SPACE = "\u200B";

commands / general / ping.js

コマンド本体です。
詳細は小項目に分けて解説していきます。

より詳しく調べたい人はこの辺見ると良いと思います。
Creating slash commands | discord.js Guide

// Discord
const Discord = require("discord.js");

// app-root-path
const app_root = require("app-root-path");

const config = require(app_root + "/config.js");
const string = require(app_root + "/strings/string.js");

// pingコマンド
module.exports = {
  // スラッシュコマンド登録
  data: new Discord.SlashCommandBuilder()
    .setName(string.PING)
    .setDescription(string.PING_DESCRIPTION)
  ,

  // コマンド処理
  async execute(interaction, client) {
    // コマンド実行ログ
    console.log("/" + string.PING);

    // Embed
    const embed = new Discord.EmbedBuilder()
      .setTitle(string.EMBED_TITLE_SUCCESS)
      .setDescription(string.ZERO_SPACE)
      .addFields([
        { name: "WebSocket", value: `**${client.ws.ping} ms**` },
        { name: "コマンド受信", value: `**${new Date() - interaction.createdAt} ms**` },
      ])
      .setColor(config.colors.success)
      .setTimestamp();

    // 返信する
    interaction.reply({ embeds: [embed] });
  },
}

(10) // スラッシュコマンド登録

このdataの中で登録してるのがスラッシュコマンドの名前や説明文、オプション(引数)になります。
最後のカンマが若干違和感あるかもですが、複雑なコマンドを作ろうとするとこの辺がゴチャゴチャしてくるので、自分はインデントをわかりやすくするために改行してます。

オプションを追加しようと思った時はこの辺りが参考になります。
Advanced command creation | discord.js Guide

(15) // コマンド処理

コマンドのメイン処理です。
Discord上でコマンドを実行したら、紆余曲折を経て最終的にはここの中身が実行されます。

DiscordにURL貼るとたまに出てくる埋め込みみたいな奴を「Embed」と言うんですが、それを任意のフォーマットで作成して投稿できます。
Discord botの面白いというか、工夫次第で色々使い道がある所だと思います。

返信もEmbedで返信する形もあれば、普通のリプライもあります。
そもそもリプライじゃなくてチャンネルに投稿するだけもできますし、コマンド使った本人にしか見えない返信方法もあります。

エラーが発生した時

前回以上に今回はエラーが発生する可能性が高いと思います。
私の掲載ミスがなければコピペで動くはずですが、万が一ミスってたらごめんなさい!

エラーが起きた時の基本は「ログを読む」です。
英語で長々と書いてあるので心が折れがちですが、慣れるとナナメ読みでいけます。

ナナメ読みのコツは「心当たりのあるファイルを探す」ことかなーと思ってます。
Node.jsアプリの場合、例えばEmbedに渡すパラメータが間違っていた場合Discord.js側のエラーが出ます。
が、道中で自分が作成したファイルが登場してくるはずなので、そこをしっかりチェックしましょう!

エラーログがないけど上手く動かない場合は、処理の随所随所でconsole.logを使って変数を確認しましょう。
コマンド登録まではいけるけど、実行した後でDiscord上でエラーが帰ってくる場合は基本こっちのパターンだと思います。

プログラミングはエラーを読んで原因箇所を特定する力とGoogleで目的の物を検索する力が大事だと思いますので、簡単な内容のうちから調べる癖をつけていきましょう!

次回は私が作ったDiscordで実用性バツグンの「ゲームするメンバー募集機能」を作る為に、要件の整理と設計について解説していきます。