マテ茶を飲もう

ネットの海を泳いでも見つからなかった情報を残します

Discord.jsで新メンバーが入った時の処理を自動化した

僕が運営しているスキーチーム用のbotをdiscord.js V14で作ったので、その概要と作成方法を共有。

前提

チームの入会フォーム(Google Form)が提出されると、discordサーバーへの招待リンクがメールで届くようになっている。discordサーバーは大まかにコーチ&現役フォルダーと現役フォルダーに分かれ、セキュリティーの観点から、○期ロールが付与されるまでの間は前者しか閲覧できないようになっている。discordのユーザーネームは本名と無関係に設定している人が多いので、僕がこんな感じでやっていた:

新入生がコーチ&現役フォルダー内にある自己紹介チャンネルに投稿 → 管理者が個人情報シート(入会フォームの回答をまとめたspreadsheet)を見て何期生かを確認 → 新入生のニックネームを本名(○期)に変更 & ○期ロールを付与 → 個人情報シートに新入生のdiscord IDを入力

5分もあれば終わる処理だけど、まぁまぁ面倒くさいので、勉強を兼ねてbotを作ってみることにした。

目標

  • 上記フローを自動化する。
  • 新メンバーと管理者のみが閲覧できる新入生ウェルカムチャンネルを作り、いわゆる ようこそ画面 の代わりとする。新たにサーバーに入った新入生は、まずこのチャンネルと自己紹介チャンネルのみ閲覧できるようになっており、新入生ウェルカムチャンネルに自分の名前を入力すると他のチャンネルも閲覧できるようになる。その後、ウェルカムチャンネルはアクセス出来なくなる。

実行環境

  • マシン:Ventura 13.4 のMac Mini
  • エディター:VS Code
  • インストールが必要なパッケージ
    • Node.js:今回はv18.15.0を使った
    • discord.js:v14を推奨
    • dotenv
    • axios
    • nodemon

コード

全体構成

今回の機能に関係するものを抜き出した。(こういうツリー構造をスムーズに書くにはどうすれば良いんでしょう。ターミナルでtreeコマンドを打ち、その出力を<pre>タグ内に貼り付けているけど、そのままだと表示がズレる。)

discord_bot
├── .env
├── .gitignore
├── events
│     ├── guildMemberAdd.js
│     ├── messageCreate.js
│     └── ready.js
├── index.js
├── newMember
│     ├── indexForNewMember.js
│     └── modifyDiscord.js
└── node_modules

全体の流れは次の通り:

サーバーに新メンバーが入る
↪︎(1) guildMemberAdd.js が実行され、NewMemberロールを付与する
↪︎新メンバーが新入生ウェルカムチャンネルに名前を送信する
↪︎(2) messageCreate.js が実行され、データのチェック, discordの各種設定(ニックネームの変更, ロールの変更など), 個人情報シートの更新を行う。
↪︎新メンバーが他のチャンネルを閲覧できるようになり、新入生ウェルカムチャンネルへのアクセスは失う。

各機能の解説

(0)根幹ファイル

nodemonで常時動かしているのはindex.js。なお、セキュリティー上公開できない情報(トークン, サーバーIDなど)は全て.envファイルに保存してある。それを別ファイルで用いたいときは、process.env.TOKENのように書く。

require('dotenv').config()
const { Client, Collection, Events, IntentsBitField } = require("discord.js")
const fs = require('node:fs')
const path = require('node:path')

//今回のbotに必要な権限を与える
const client = new Client({
    intents: [
        IntentsBitField.Flags.Guilds,
        IntentsBitField.Flags.GuildMembers,
        IntentsBitField.Flags.GuildMessages,
        IntentsBitField.Flags.MessageContent,
    ]
})

//各種イベントはeventsフォルダーに格納しているので、それを全て読み込む
const eventsPath = path.join(__dirname, 'events')
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'))

//readyイベントはbotの起動時のみ、その他のイベントには常時反応できるようにする
for (const file of eventFiles){
    const filePath = path.join(eventsPath, file)
    const event = require(filePath)
    if(event.once){
        client.once(event.name, (...args) => event.excute(...args))
    }else{
        client.on(event.name, (...args) => event.excute(...args))
    }
}

//botを起動
client.login(process.env.TOKEN)

(1)guildMemberAddイベント

サーバーに新メンバーが入ると、以下のコードが実行される。NewMemberロールのIDは書き換えてください。

guildMemberAdd.js

const {Events} = require('discord.js')

module.exports = {
   name: Events.GuildMemberAdd,
   async excute(member){
      //臨時のロールNewMemberを付与する。
      await member.roles.add('1116738049548755044')
   }
}

(2)messageCreateイベント

サーバー内のいずれかのチャンネルにメッセージが送信されると、以下のコードが実行される。そのチャンネルが新入生ウェルカムチャンネルであり、送信者がbot以外のときのみ、indexForNewMember.js ファイルに処理を引き渡す。

messageCreate.js

const {Events} = require('discord.js')

module.exports = {
    name: Events.MessageCreate,
    async excute(message){
        //新入生ウェルカムチャンネルにのみ反応する
        if(message.channelId==process.env.WELCOME_CHANNEL_ID && !message.author.bot){
            const indexForNewMember = require('../newMember/indexForNewMember.js')
            indexForNewMember(message)
        }
    }
}

indexForNewMember.js ファイルは以下の動作をまとめたもので、①の動作は同ディレクトリーの modifyDiscord.js ファイルで行っている。

  • 入力された名前が個人情報シートに記録されているかをチェック
  • ①新入生のロールとニックネームを変更する
  • 個人情報シートに新入生のdiscord IDを追記する
  • 新入生ウェルカムチャンネルの不要な投稿を削除する

indexForNewMember.js

const {Events} = require('discord.js')
const axios = require('axios')

module.exports = async (message)=>{
    const name = message.content
    const requestToPersonalInfoSheet = await axios.get(`${process.env.PERSONAL_INFO_SHEET}/search?sheet=名簿&氏名=${name}`)
    const record = requestToPersonalInfoSheet.data

    if(record.length===0){
        message.reply(`❌ ${name}さんのデータは個人情報シートに記録されていません。表記を確認して、もう一度お試しください。`)
    }else{
        const memberID = message.author.id
        const gen = record[0]['代'] //integer

        //新入生のDiscord設定を変更(ロールの変更, ニックネームの変更)。
        const modifyDiscord = require('./modifyDiscord.js')
        await modifyDiscord(message, name, gen)

        //個人情報シートを更新
        const discordInfoSheet = axios.post(`${process.env.PERSONAL_INFO_SHEET}?sheet=discord`, {名前: name, 代: gen, ID: memberID})

        //ウェルカムchの不要なメッセージを削除する
        const messagesToDelete = await message.channel.messages.fetch({after: '1116774827383066684'})
        message.channel.bulkDelete(messagesToDelete)
    }
}

modifyDiscord.js

const {Events} = require('discord.js')

module.exports = async (message, name, gen) => {
    //gen期のロールを付与する。ToDo管理botロールのランクを○期より上に設定しておく必要がある。
    const role = message.guild.roles.cache.find(role => role.name===`${gen}期`)
    if (!role) {
        console.log(`${gen}期ロールが存在しません。`); 
        return;
    }

    if(!message.member.roles.cache.has(role.id)){
        try {
            await message.member.roles.add(role)
        } catch (error) {
            console.log(`Error adding role to new member: ${error}`)
        }
    }

    //ニックネームを変更。これもToDo管理botより高いロールが既に付与されている人に対してはエラーを吐く。
    try {
        await message.member.setNickname(`${name}(${gen}期)`) 
    } catch (error) {
        console.log(`Unable to change the nickname. Error: ${error}`)
    }

    await message.reply(`${name}さんのDiscord設定が完了しました。他のチャンネルも覗いてみてください!`)

    //NewMemberロールを削除
    await message.member.roles.remove('1116738049548755044')
}

ソースコード

GitHubにアップロード済み。元々はdiscordサーバーで完結するtodo管理機能を作ろうと思っていたので、discord_todo_botというフォルダー名になっている。この記事に書かれていないGitHub上のコードは、大体todo機能関連。

クラウドサーバーで常時起動

別の記事を参照。