從零開始的Discord機器人(3) - 改善指令架構
本篇會將改善目前Discord機器人的指令架構,使其更易於擴充與維護。
在上一篇中,我們幫機器人加上斜線指令,目前機器人已經具有接收指令與回覆的能力了。但我相信你一定不滿足於現狀,打算幫機器人加上更多指令。
然而很快地你發現了問題:一開始加上一兩個指令,好像沒遇到什麼問題,但隨著指令越加越多,漸漸地整個機器人的程式碼變得又臭又長,不僅不利於維護,同時也難以擴充。因此在本文中,我們將會改善機器人的指令架構,讓機器人本體與指令分開,以利於擴充指令。
1. 從指令本體開始
首先,我們先來處理指令本體的部分。
到目前為止,我們新增指令的方式都是全部寫在index.js裡。如果只有少少幾個指令的話還行,當指令越來越多時便會難以閱讀與維護。所以接下來我們要把指令都拆分成各自的檔案。
新增一個commands資料夾,這個資料夾會用來存放所有斜線指令的檔案。目前我們有ping與hello兩個指令,因此我們在commands內新增ping.js與hello.js兩個檔案。
接下來以ping.js為例,我們要使用module.exports來匯出所有需要的東西,包含SlashCommandBuilder以及接收interactionCreate的事件要執行的動作,完整的檔案會長這樣:
const { SlashCommandBuilder } = require ('discord.js')
module.exports = {
data: new SlashCommandBuilder()
.setName ('ping')
.setDescription('Replies with "Pong!"')
.setDefaultMemberPermissions('0'),
async execute (interaction) {
interaction.reply('pong!')
}
}
之後要新增指令時,只要依照和上面SlashCommandBuilder與execute的結構就可以輕易擴充了。讓我們也完成hello.js的部分吧:
const { SlashCommandBuilder } = require ('discord.js')
module.exports = {
data: new SlashCommandBuilder()
.setName ('hello')
.setDescription('Say hello to someone"')
.setDefaultMemberPermissions('0')
.addUserOption(option =>
option
.setName('user')
.setDescription('The user to say hi to')
.setRequired(false)
),
async execute (interaction) {
let user = interaction.options.getUser('user')
interaction.reply(`Hello <@${user.id}>!`)
}
}
2. 註冊指令也要聰明一點
除了指令之外,我們每新增一個指令時,都會使用client.application.commands.create
來註冊指令,但隨著後續指令的增加,每次新增指令都要加上一條註冊指令顯然不是一個好做法。
我們先來做一點事前準備。由於這次的目標是使用在單一伺服器的機器人,因此我們直接把伺服器的頻道ID寫在config.js內。另外,我們還需要到Discord Developer Portal複製機器人的General Information內的Application ID,這兩個值都是我們之後會用到的。現在的config.js應該會長這樣:
{
"token": "填入機器人的Token",
"guildId": "填入伺服器的ID",
"applicationId": "填入剛剛取得的Application ID",
}
接下來,讓我們新增一個檔案deploy-commands.js,首先是require:
const fs = require('node:fs')
const path = require('node:path')
const { REST } = require("@discordjs/rest")
const { Routes } = require("discord.js")
const {token, applicationId, guildId} = require('./config.json')
再來,我們要新增一個function來幫助我們取得檔案:
function getFiles (dir) {
const files = fs.readdirSync (dir, {
withFileTypes: true
})
let commandFiles = [];
for (const file of files) {
if (file.isDirectory()) {
commandFiles = [
...commandFiles,
...getFiles(`${dir}/${file.name}`)
]
}
else if (file.name.endsWith(".js")) {
commandFiles.push (`${dir}/${file.name}`)
}
}
return commandFiles
}
這個getFiles的功能是,指定一個資料夾路徑,function會幫我們把指定路徑內所有的JS檔都找出來(包含所有子目錄)。我們接下來要做的事情是,用getFiles來取得commands資料夾內的所有指令並一一註冊。首先是取得指令的資料:
let commands = [];
const commandFiles = getFiles ('./commands');
for (const file of commandFiles) {
const command = require (file);
commands.push (command.data.toJSON ());
}
有了指令的資料,我們就可以開始註冊指令了:
const rest = new REST ({version: '10'}).setToken (token)
rest.put (Routes.applicationGuildCommands (applicationId, guildId), { body: commands })
.then (() => console.log ('Successfully registered application commands!'))
.catch (console.error)
如果你想要讓所有伺服器都能使用指令,只要將Routes.applicationGuildCommands
改成Routes.applicationCommands
,如此一來就不需要用到guildId。之後當我們新增指令之後,只要執行一次node deploy-commands.js
,就能一次註冊所有指令了。
3. 執行的部分也要改寫
接下來,我們也來改寫一下InteractionCreate的部分,目前應該是長這樣:
client.on(Events.InteractionCreate, interaction => {
if (!interaction.isChatInputCommand ()) return;
if (interaction.commandName === "ping") {
interaction.reply ("Pong!");
}
if (interaction.commandName === "hello") {
const user = interaction.options.getUser ('user');
interaction.reply (`Hello <@${user.id}>`);
}
});
在這裡,我們會根據interaction的commandName來做相對應的動作,但因為我們在上面已經把每個指令要執行的動作都放在各自的execute裡面了,所以我們可以改寫成根據commandName找到相對應的指令並呼叫execute。在這裡我們要使用Discord.js內的Collection
實現這一功能。
Collection其實就是JavaScript內建的Map物件的擴充。簡短說明一下,Map是一種鍵值對(key-value pairs)的物件,也就是Map內每一個key都有其對應的value。所以我們可以利用Collection(或者說Map)這種特性,當我們以commandName為key時,就能得到相對應的指令了。
首先我們先來建立一個可以生成我們需要的Collection的function吧:
function getCommands (dir) {
let commands = new Collection ();
const commandFiles = getFiles (dir);
for (const commandFile of commandFiles) {
const command = require (commandFile);
commands.set (command.data.toJSON ().name, command);
}
return commands;
}
在這裡也有使用到getFiles,你可以將getFiles與getCommands都整理到一個額外的JS檔內,之後在其他JS檔內只要require就能重複使用,但礙於篇幅關係這裡就不特別整理了。
有了getCommands之後就能生成Collection了,我們將它放在client.commands
內:
client.commands = getCommands ('./commands');
接下來是改寫InteractionCreate的部分,首先我們根據interaction.commandName
從client.commands
內得到相應的command並呼叫其execute就完成了。改寫之後會長這樣:
client.on(Events.InteractionCreate, interaction => {
let reaction;
if (interaction.isChatInputCommand()) {
reaction = client.commands.get(interaction.commandName);
}
try {
if (interaction.replied) return
reaction.execute(interaction);
}
catch (error) {
console.error(error);
}
});
這裡有順便做一些小調整,之前的寫法中,如果取得的interaction不是斜線指令就會直接結束。調整後,如果是斜線指令就會取得相應的reaction。這是為了後續能夠方便擴充,之後也能根據interaction的類型不同(Button、Modal等),到不同的Collection內取得相應的資料並執行。
最後,你可以開始運作你的機器人了。
4. 接下來要做的事情
到目前為止,我們改善了機器人的架構,後續如果需要新增或管理斜線指令,都只會在commands資料夾的範圍內操作,不會影響到機器人本體。但接下來還有可以改善的地方,也許你有發現,機器人可以監聽的事件其實有很多,並不只有ClientReady與InteractionCreate,只要是使用者的操作基本上機器人都能監聽。
因此,接下來我們也會像拆分指令那樣,將監聽事件從機器人本體分離。如此一來,整理機器人架構就算完成了。
感謝你看到這邊,我們下次見。