ひぃの雑多書き

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

【v14】discordjsのすゝめ 第4回「新機能の設計と下準備(リアクションハンドラの作成)」

こんにちは。ひぃです。

discordjsもある程度理解できたということで書き始めた「discord botの作り方」の第4回です。
今回はリアクションハンドラを作りつつ、コマンドの設計をしていきます。

◇環境
node: 16.13.2
discordjs: 14.7.1

4.新機能の設計と下準備(リアクションハンドラの作成)

今回目指す成果

今回はリアクションの付与・削除をbotで検知できるようにしていきます!

また、次回解説予定のコマンドでは引数も増え、より複雑になります。
現在の知識をベースに、自分で簡単なコマンドを作ってみるのをオススメします!

ファイル構成

現在のファイル構成はこんな感じです。

startupフォルダの中に下記のファイルを作成してください。

  • reaction_add_handler.js
  • reaction_remove_handler.js

なお、general配下にあるhello.jsは身内に教える際に課題で作ったコマンドです。

要件だけ紹介するとこんな感じです。

  • 必須の引数を1つ作成し、『朝』『昼』『夜』から選べるようにする
  • 『朝』の場合は『おはようございます!』と返信をする
  • 『昼』の場合は『こんにちは!』と返信する
  • 『夜』の場合は『こんばんは!』と返信する

index.js

長くなってきたので追加行だけ記載してます。
今回追加するハンドラを設定してるだけです。

『スラッシュコマンド登録処理』『インタラクション(コマンド)受信時の処理』辺りの後に書いてあげると良いと思います。

/** 
 * リアクション追加時の処理
 */
const reaction_add_handler = require(app_root + "/startup/reaction_add_handler.js");
reaction_add_handler.call(client);

/** 
 * リアクション削除時の処理
 */
const reaction_remove_handler = require(app_root + "/startup/reaction_remove_handler.js");
reaction_remove_handler.call(client);

reaction_add_handler.js

新規ファイルです。
botが参加しているサーバで誰かがリアクションした時に呼ばれます。

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

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

exports.call = async function(client) {
  client.on('messageReactionAdd', async (reaction, user) => {
    // botの場合は処理を中断
    if (user.bot) return;

    // DMの場合は処理を中断
    if (reaction.message.guildId == null) return;

    try {
      // 最新の情報を取得
      let channel = reaction.message.guild.channels.cache.get(reaction.message.channel.id);
      let message = await channel.messages.fetch(reaction.message.id);

      // コマンド以外のリアクションは無視する
      if (message.interaction == null || !message.interaction.type === Discord.InteractionType.ApplicationCommand) return;
      
      // コマンド名 サブコマンド部分を切り取る
      const command_name = message.interaction.commandName.split(string.HALF_SPACE)[0];

      // コマンド別に処理を行う
      switch (command_name) {
        // inviteコマンド
        case string.INVITE:
          console.log("Reaction Handle : invite (add)");
          break;

        default:
          console.log("Reaction Handle : default (add)")
          return;
      }
    } catch (error) {
      console.error(error);
    }
  });
}

現在解説してる範囲だとサブコマンドを取り扱うコマンドはないのですが、あってもエラーにはならないので残してます。

また、discord.jsで取得できる大抵の物に言える話なんですが、『cache.get』と『fetch』は意識して使い分けできるようになるとgoodです!

cache.getを使うメリットは「既に持ってるキャッシュから拾ってくるのでawaitする必要がなく、処理が高速」、デメリットは「キャッシュにない情報は拾ってこれない」ということです。
fetchを使うメリットは「最新の情報を拾ってこれる」ことで、デメリットは「非同期処理なのでawaitで待つ必要があり、処理に時間がかかる」かなと。

reaction_remove_handler.js

こちらも新規ファイルです。
add_handlerと記載内容は殆ど一緒なので、説明は割愛します。

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

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

exports.call = async function(client) {
  client.on('messageReactionRemove', async (reaction, user) => {
    // botの場合は処理を中断
    if (user.bot) return;

    // DMの場合は処理を中断
    if (reaction.message.guildId == null) return;

    try {
      // 最新の情報を取得
      let channel = reaction.message.guild.channels.cache.get(reaction.message.channel.id);
      let message = await channel.messages.fetch(reaction.message.id);

      // コマンド以外のリアクションは無視する
      if (message.interaction == null || !message.interaction.type === Discord.InteractionType.ApplicationCommand) return;

      // コマンド名 サブコマンド部分を切り取る
      const command_name = message.interaction.commandName.split(string.HALF_SPACE)[0];

      // コマンド別に処理を行う
      switch (command_name) {
        // inviteコマンド
        case string.INVITE:
          console.log("Reaction Handle : invite (remove)");
          break;

        default:
          console.log("Reaction Handle : default (remove)")
          return;
      }
    } catch (error) {
      console.log(error);
    }
  })
}

string.js

既存ファイルの更新です。

前述の「hello」コマンドの文言や、次回作成予定の「invite」コマンドの文言もあります。

/** 
 * コマンド名、説明文
 */
exports.HELLO = "hello";
exports.HELLO_DESCRIPTION = "挨拶をします。";

exports.INVITE = "invite";
exports.INVITE_DESCRIPTION = "ゲーム等の募集を行います。";

exports.PING = "ping";
exports.PING_DESCRIPTION = "botが正常に動作しているかをチェックします。";

/** 
 * コマンドオプション
 */
exports.HELLO_OPTION_TIMES = "times";
exports.HELLO_OPTION_TIMES_DESCRIPTION = "時間帯を選択してください。";

exports.HELLO_OPTION_TIMES_NAME_MORNING = "朝";
exports.HELLO_OPTION_TIMES_NAME_NOON = "昼";
exports.HELLO_OPTION_TIMES_NAME_NIGHT = "夜";

exports.NUMBER_MORNING = 1;
exports.NUMBER_NOON = 2;
exports.NUMBER_NIGHT = 3;

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

/** 
 * MESSAGE
 */
exports.HELLO_MESSAGE_MORNING = "おはようございます!";
exports.HELLO_MESSAGE_NOON = "こんにちは!";
exports.HELLO_MESSAGE_NIGHT = "こんばんは!";

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

リアクションの取得確認

ここまで完成したらbotを起動して動作確認をしましょう!

リアクションをつけた時にログにメッセージが出力されればOKです。

ちゃんとソースを読めばすぐわかるとは思いますが、今回若干のイジワルを入れています。
エラーは出ないけどログにメッセージが出ないって人はソースコードをちゃんと読んでみてね。

新機能の設計

さて、ここまでできたら新機能の設計を簡単にしていきましょう!

解説では要件の整理+αくらいの粒度でやりますが、自信がない人はEmbedのイメージ図とか書いてもいいかも。
自分が「よーし実装するぞ!」ってゴリゴリソースを書いていけるようになる所まで調べたり決めたりするのが良いと思います。

要件定義

今回は「Discordサーバ内でゲームの募集をするのに使えるコマンド」を作ります。
今回のコマンドは既に身内サーバで運用してるので、参考用のイメージがあります。

募集文を設定してコマンドを実行するとEmbedにその内容が表示されて、リアクションを付けることで参加意思の表明ができる、みたいな感じです。
あそこまで長文ではない想定ですが、Twiplaみたいな。「参加」「興味アリ」「不参加」で選べると良さそう。

やりたいことが決まったら、どうやれば実現できるかを考えていきます。
Googleはもちろん、公式リファレンスやこの辺りを調べると参考になるかと思います。

やりたいこと逆引き集 - Discord.js Japan User Group

コマンド部

コマンド実行時点でやりたいことを切り分けます。

まず、募集タイトルと募集の詳細を引数で受け取りたいですね。
この辺見ると書いてあるのですが、コマンドには引数を設定することができます。

Parsing options | discord.js Guide

また、最初にリアクションを付ける人がつけやすいようにbotがリアクションを付けておきたいですね。

リアクション部

今回作成したreaction_handlerを使うと「どのメッセージ(コマンド)に」「誰が」「どのリアクションをしたか」を取得できます。

これを使って、「参加用のリアクションを押したら参加一覧に名前を載せる」みたいなことをやりましょう。
ただし、一人のユーザはどれか一つのリアクションだけできるようにしたいですね。
「参加を押したけどやっぱり無理そう!」ってなって不参加を押した時は自動で参加一覧からも消す、みたいな。

あとは、募集を終了することもできるようにしましょう。
リアクションは募集中のみ反映するようにして、募集が終わっている時は何も起きないようにしたいですね。

最後に

今回はリアクションハンドラの作成をしつつ、次に作りたいコマンドの内容を整理しました。
慣れてくると「趣味プログラミングなんだから設計書とか作るの面倒なだけやん~~~」ってなるんですけど、どういう手段を使えば実現可能かくらいは調べておくと後々楽なのでオススメです!

次回は実際にinviteコマンドを作っていきます。