discord.js v14 でVC入退室ログを特定のテキストチャンネルに送信してみた


ただ記事の連続投稿的なのを達成するためだけに書きます()
特に書くこと無いので、作って便利だったものでも布教?していこうかなと思います。
今回取り上げるのは、「VC入退室ログ機能」です。
データベースは特に使いませんが、DiscordサーバーのプライベートBOTではない場合(今回は取り上げません)やオンオフ機能をつけたい場合(記事後半でとりあげます)はjsonファイルなどデータベースが必要です。
BOTを一から作ることは想定していないので初めてBOTを作る場合、この記事は役に立たないと思います。(枠組み作ってから挑戦してみてください。)
ちなみにコードはきちゃないです。


環境(使う部分のみ)

Node.js v16.14.2
discord.js v14.14.1

なぜこんな機能を作ろうと思ったのか

サーバーに人が集まってきて、誰がVCに入ったのかもわからなくなってきたので、ログとして出力すれば誰がいつどこに入ったのかわかるのではないか!と思ったからです。

注意事項

今回紹介する機能は、コードを書き換えないと全てのサーバーのVC入退室ログが一つの指定されたチャンネルに送信されます。(例えば、サーバーAとサーバーBがあったとして、サーバーAのテキストチャンネルAにVC入退室ログの出力を指定した場合、サーバーBでVCに入ってもサーバーAのテキストチャンネルAにログが表示されるということ)
(気力があったら回避方法後述していると思います。)(かる〜く後述しました)
また、ボイスチャンネルがBOT側から見れない場合、VC入退室ログは出力されません。

コード(ただログをチャンネルに送信したいだけの場合)

client.on("voiceStateUpdate", (oldState, newState) => {
    var channel = oldState.member.guild.channels.cache.get("ここに出力するテキストチャンネルのID");
      if (oldState.channelId === null && newState.channelId !== null) {
        return channel.send(
          `**入室** ${oldState.member.user.displayName}\n${
            newState.channel.name
          } <t:${Math.floor(Date.now() / 1000)}:T>`
        );
      } else if (oldState.channelId !== null && newState.channelId === null) {
          return channel.send(
            `**退出** ${newState.member.user.displayName}\n${
              oldState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
      } else if (oldState.channelId !== null && newState.channelId !== null) {
        if (oldState.channelId === newState.channelId) {
          return;
        } else {
            return channel.send(
              `**移動** ${newState.member.user.displayName}\nfrom : <#${
                oldState.channelId
              }> \nto : <#${newState.channelId}> <t:${Math.floor(
                Date.now() / 1000
              )}:T>`
            );
        }
      } else {
        return;
      }
  });

"ここに出力するテキストチャンネルのID"の部分を指定された通り指定してください。
Stringで指定なので、チャンネルIDはダブルクオーテーション→"で囲ってください。
簡単に解説すると、
oldState.channelId === null && newState.channelId !== null
ここは、oldState.channnelIdがない場合、そしてnewState.channelIdが無く無い(ある)場合に処理を行うというコードです。
つまり元々VCチャンネルに入っていなくてVCチャンネルに入った場合に処理を行うということです。(入室)
oldState.channelId !== null && newState.channelId === null
ここは、oldState.channnelIdが無く無い(ある)場合、そしてnewState.channelIdがない場合に処理を行うというコードです。
つまり元々VCチャンネルに入っていたけどVCチャンネルを抜けた場合に処理を行うということです。(退室)
oldState.channelId !== null && newState.channelId !== null
ここは、oldState.channnelIdが無く無い(ある)場合、そしてnewState.channelIdが無く無い(ある)場合に処理を行うというコードです。
つまり元々VCチャンネルに入っていて、VCチャンネルに入った場合に処理を行うということです。(移動)
なぜかずっと同じVCにいるのに移動判定になることがあったので、
oldState.channelId === newState.channelId
の場合処理をしないと指定して回避しています。
最後の
}else{
return;
}
は一応書いてますが多分いりません。

このコードを実行すると↓こうなります(後述する機能を使っているため一部変な文字列が入っています。)

コード(カスタムメッセージを含める)

const custom = require("./custom.json");
client.on("voiceStateUpdate", (oldState, newState) => {
    var channel = oldState.member.guild.channels.cache.get("ここに出力するテキストチャンネルのID");
    var custommessage = custom[oldState.member.id];
      if (custommessage === 0) {
        return;
      }
      if (oldState.channelId === null && newState.channelId !== null) {
        if (custommessage === undefined) {
          return channel.send(
            `**入室** ${oldState.member.user.displayName}\n${
              newState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
        }
        return channel.send(
          `**入室** ${oldState.member.user.displayName}\n${custommessage}\n${
            newState.channel.name
          } <t:${Math.floor(Date.now() / 1000)}:T>`
        );
      } else if (oldState.channelId !== null && newState.channelId === null) {
          return channel.send(
            `**退出** ${newState.member.user.displayName}\n${
              oldState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
      } else if (oldState.channelId !== null && newState.channelId !== null) {
        if (oldState.channelId === newState.channelId) {
          return;
        } else {
            return channel.send(
              `**移動** ${newState.member.user.displayName}\nfrom : <#${
                oldState.channelId
              }> \nto : <#${newState.channelId}> <t:${Math.floor(
                Date.now() / 1000
              )}:T>`
            );
        }
      } else {
        return;
      }
  });

ただVC入退室ログを出力するだけの機能にカスタムメッセージを追加したものです。
custom.jsonの中身は

{
  "847762922734223410": "平坂ハジメ",
 "760825049468633110": "ぐちぐちゃ"
}

こんな形式で保存してあります。
"DiscordのユーザーID":"カスタムメッセージ"
カスタムメッセージの部分では、"あ\nい"などのように指定することで、カスタムメッセージを改行(\n)することができます。
もし、カスタムメッセージ部分がNumberの0に指定されていた場合はそのユーザーのログは取得しないようにしています。

コード(カスタムメッセージを含め、指定したチャンネルはログを取得しない)

const custom = require("./custom.json");
client.on("voiceStateUpdate", (oldState, newState) => {
    var channel = oldState.member.guild.channels.cache.get("ここに出力するテキストチャンネルのID");
    var custommessage = custom[oldState.member.id];
      if (custommessage === 0) {
        return;
      }
      if (newState.channelId === "ここに除外するボイスチャンネルのID") {
        return;
      }
      if (oldState.channelId === "ここに除外するボイスチャンネルのID") {
        return;
      }
      if (oldState.channelId === null && newState.channelId !== null) {
        if (custommessage === undefined) {
          return channel.send(
            `**入室** ${oldState.member.user.displayName}\n${
              newState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
        }
        return channel.send(
          `**入室** ${oldState.member.user.displayName}\n${custommessage}\n${
            newState.channel.name
          } <t:${Math.floor(Date.now() / 1000)}:T>`
        );
      } else if (oldState.channelId !== null && newState.channelId === null) {
          return channel.send(
            `**退出** ${newState.member.user.displayName}\n${
              oldState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
      } else if (oldState.channelId !== null && newState.channelId !== null) {
        if (oldState.channelId === newState.channelId) {
          return;
        } else {
            return channel.send(
              `**移動** ${newState.member.user.displayName}\nfrom : <#${
                oldState.channelId
              }> \nto : <#${newState.channelId}> <t:${Math.floor(
                Date.now() / 1000
              )}:T>`
            );
          }
      } else {
        return;
      }
  });

先に言っておきますがもっと良い書き方があります多分。
"ここに除外するボイスチャンネルのID"の部分を指定された通りに指定してください。
こんな感じで指定したサーバー以外からは取得しないようにするとかもできます(多分oldState.guildIdとか使えばできる。)

コード(カスタムメッセージを含め、指定したチャンネルはログを取得せず、ログの取得機能をオンオフし、スパムを防止する)

const custom = require("./custom.json");
const settings = require("./settings.json");
client.on("voiceStateUpdate", (oldState, newState) => {
    var channel = oldState.member.guild.channels.cache.get("1192128304283668614");
    var custommessage = custom[oldState.member.id];
    if (settings["voicenotice"] === 1) {
      if (custommessage === 0) {
        return;
      }
      if (oldState.channelId === null && newState.channelId !== null) {
        if (newState.channelId === "1208438241213284402") {
          return;
        }
        if (oldState.member.user.displayName.match(/<@|@everyone|http/)) {
          return channel.send(
            `**入室** [セキュリティの関係上表示できない名前]\n${
              newState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
        }
  
        if (custommessage === undefined) {
          return channel.send(
            `**入室** ${oldState.member.user.displayName}\n${
              newState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
        }
        return channel.send(
          `**入室** ${oldState.member.user.displayName}\n${custommessage}\n${
            newState.channel.name
          } <t:${Math.floor(Date.now() / 1000)}:T>`
        );
      } else if (oldState.channelId !== null && newState.channelId === null) {
        if (oldState.channelId === "1208438241213284402") {
          return;
        }
        if (newState.member.user.displayName.match(/<@|@everyone|http/)) {
          return channel.send(
            `**退出** [セキュリティの関係上表示できない名前]\n${
              oldState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
        } else {
          return channel.send(
            `**退出** ${newState.member.user.displayName}\n${
              oldState.channel.name
            } <t:${Math.floor(Date.now() / 1000)}:T>`
          );
        }
      } else if (oldState.channelId !== null && newState.channelId !== null) {
        if (oldState.channelId === newState.channelId) {
          return;
        } else {
          if (newState.member.user.displayName.match(/<@|@everyone|http/)) {
            return channel.send(
              `**移動** [セキュリティの関係上表示できない名前]\nfrom : <#${
                oldState.channelId
              }> \n to : <#${newState.channelId}> <t:${Math.floor(
                Date.now() / 1000
              )}:T>`
            );
          } else {
            return channel.send(
              `**移動** ${newState.member.user.displayName}\nfrom : <#${
                oldState.channelId
              }> \nto : <#${newState.channelId}> <t:${Math.floor(
                Date.now() / 1000
              )}:T>`
            );
          }
        }
      } else {
        return console.log("false");
      }
    }
  });

settings.jsonの中身は

{"voicenotice":1}

です。
(正直custom.jsonにつっこんでもいい)
jsonの書き換えは今回は取り上げないので自力でがんばってください。
(筆者はスラッシュコマンドとfsモジュールを使用しました。)
オンオフ機能は、
settings["voicenotice"] === 1
で指定しています。
settingsのvoicenoticeが1(Number)の時に処理をするという感じです。
なのでsettingsのvoicenoticeが1(Number)じゃなければ何でもオフになります。
(筆者はオフにする場合はundefinedにしてます)
スパム対策とは具体的になにをしてるかというと、もしdisplayNameが「<@ユーザーid>」や「httpから始まるURL」だった場合、メンションしてしまったり意図せず危険なURLを送ってしまったりするためそういうことができないようもし上記に該当する場合は[セキュリティ上表示できない名前]というログを残すようにしてあります。
vcを高速で抜けたり入ったりしたら、APIのレート制限(1つのアプリケーション(BOT)につき1時間に1000メッセージまで(確か))に引っかかる可能性があります(回避方法は書きません。自分で何とかしてください(放棄))
webhookを使うという方法も考えましたが、なんか見にくそ〜と思いやめました。
普通に使ってればまずレート制限かかることはないです。

終わりに

今回は、VC入退室ログを出力するコードを書きました。
本来一番最初にに書くべきことですが、
voiceStateUpdateはdiscordからimportしてます(一応)
何とこの記事1万文字を超えています(ビックリ)(多分コード含めてなんだけど)
話は変わりますが、この記事見やすいですか?わかりやすいですか?
あと金を出すとしたら何円ぐらい出しますか?(有料にする気は今のところないですが参考までに)
改善点とか色々コメントでお待ちしております!
前回の記事で自動翻訳botを作ってみたのでそちらもみてみてください!(discord.js v13ですが、、、)
またお会いしましょう!


この記事が気に入ったらサポートをしてみませんか?