Discord.pyの開発終了
2021年8月28日、Discord APIラッパーのひとつであるDiscord.pyの開発終了がアナウンスされました。
the_future_of_dpy.md - GitHub
開発終了したソフトウェアを使い続けることは、リスクの面から見ても推奨されません。
当初の予定では、同じPython製のAPIラッパーであるPycordを利用してDiscord.pyからのリファクタリングを行う予定でした。
ですがEmbedやスラッシュコマンドの実装が容易であり、人口が多いということでDiscord.jsへのリファクタリング作業を行いました。1
なお、この記事はGitHubリポジトリにコードが載っています。
よろしければstarを頂けると開発の励みになります!
また、実装途中でわからない部分があれば私が参考にしたページを閲覧すると解決するかもしれません。
#参考記事
Bot詳細
私の管理するDiscordサーバーでは、Minecraftをプレイしている人が多くいます。
このMinecraftのマルチサーバーをEC2でホストしているのですが、誰もログインしていないときなどはこまめに停止し料金を抑えたいという需要がありました。
そこで、AWS CLIというCLIツールを利用し、Discord内から簡単にEC2インスタンスの起動や停止を行います。
リファクタリング前の旧Bot↓
Honahuku/EC2電源管理くん
リファクタリング後↓
Honahuku/aws_discord_bot
環境設定
このBotではNode.jsとシェルスクリプトを利用しています。
環境は以下の通り
Ubuntu 20.04
Discord.js v13
Node.js v16
ソフトウェアのインストール
nodeなど実行に必要なソフトウェアのインストールを行います
nコマンドを利用し、複数バージョンの切り替えを可能にします。ここではltsのnodeを導入します。
sudo apt update
sudo apt upgrade -y
sudo apt install nodejs npm screen
sudo npm install n -g
sudo n lts
node -v
作業用ディレクトリの作成
/varに作業用ディレクトリを設定します。
mkdir /var/aws_discord_bot
cd /var/aws_discord_bot
モジュールのインストール
sudo npm install discord.js # Discord.js
sudo npm install dotenv --save # 環境変数読み込み用
sudo npm install --global eslint # JSの構文エラーチェック用
Linterのセットアップ
jsの構文エラーを発見してくれるLinterをセットアップします。
sudo npm install --global eslint
vscode拡張機能のインストール
VScodeを利用している場合は、Linter用VScode拡張機能のインストールを推奨します。
他のエディターを利用している場合は適宜対応してください。
ESLint - Visual Studio Marketplace
ESLintルールの設定
Discord.jsのユーザーガイドを参考にLinterのルールを設定します。
vim ./.eslintrc.json
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2019
},
"rules": {
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1, "maxBOF": 0 }],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"semi-spacing": ["error", {"after": true, "before": false}],
"semi-style": ["error", "last"],
"no-extra-semi": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"space-before-blocks": "error",
"space-before-function-paren": ["error", {
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}
Botをサーバーに追加
botトークンの発行
以前から使っているbotをリファクタリングするのでスキップします。
1から発行したい場合はこちらを参照してください。
サーバーIDの取得
※サーバーの管理者権限が必要です。
discordを起動し、botを導入しているサーバーのアイコンを右クリック
envファイルの作成
さきほど取得したアクセストークンとサーバーIDを格納する.env
ファイルを作成します。
vim .env
SERVER_TOKEN="your_server_id"
DISCORD_TOKEN="Your_Discord_Bot_Token"
DEV1_INSTANCE_ID="i-00000000000000000"
DEV2_INSTANCE_ID="i-00000000000000000"
DEV3_INSTANCE_ID="i-00000000000000000"
サンプルプログラム
サンプルプログラムを飛ばして実際の実装を行う場合は#aws_cliのセットアップまでスキップしてください。
スラッシュコマンド
Discordの方針転換に伴い、Discord Botはスラッシュコマンドを利用してBotを実装する必要があります。
本格的な実装を行う前にサンプルプログラムとして簡単なping-pong Botでスラッシュコマンドを実装します。
ping-pong Botではユーザーのping
というメッセージに反応しPong!
を返信します。
vim index.js
const { Client, Intents } = require('discord.js');
const dotenv = require('dotenv');
dotenv.config();
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
client.once("ready", async () => {
const data = [{
name: "ping",
description: "Replies with Pong!",
}];
await client.application.commands.set(data, process.env.SERVER_TOKEN);
console.log("Ready!");
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand()) {
return;
}
if (interaction.commandName === 'ping') {
await interaction.reply('Pong!');
}
});
client.login(process.env.DISCORD_TOKEN);
pingというスラッシュコマンドにpongと反応がありました。
フレームワーク作成
ファイル構成を以下のように変更します。これにより各コマンドを別のファイルに分けることができます。
index.js
からping.js
というモジュールを作成します。
このping.js
を格納するフォルダーをカレントディレクトリに新たに作成してください。
mkdir ./commands
const fs = require('fs')
const { Client, Intents } = require('discord.js');
const dotenv = require('dotenv');
dotenv.config();
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
const commands = {}
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'))
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
commands[command.data.name] = command
}
client.once("ready", async () => {
const data = []
for (const commandName in commands) {
data.push(commands[commandName].data)
}
await client.application.commands.set(data, process.env.SERVER_TOKEN);
console.log("Ready!");
});
client.on("interactionCreate", async (interaction) => {
if (!interaction.isCommand()) {
return;
}
const command = commands[interaction.commandName];
try {
await command.execute(interaction);
} catch (error) {
console.error(error);
await interaction.reply({
content: 'There was an error while executing this command!',
ephemeral: true,
})
}
});
client.login(process.env.DISCORD_TOKEN);
module.exports = {
data: {
name: "ping",
description: "Replies with Pong!",
},
async execute(interaction) {
await interaction.reply('Pong!');
}
}
コマンドの選択肢を追加
コマンドの選択肢を追加する場合のサンプルを以下に示します。
module.exports = {
data: {
name: "start",
description: "サーバーを起動します",
options: [{
type: "STRING",
name: "server",
description: "起動するサーバーを入力してください",
required: true,
choices: [
{ name: "Dev1", value: "dev1" },
{ name: "Dev2", value: "dev2" },
{ name: "Dev3", value: "dev3" },
]
}],
},
AWS_CLIのセットアップ
discord.jsの動作にも慣れたところでAWS CLIをインストールします。
sudo apt install awscli
EC2管理用ポリシーの作成
今回操作するマシンは私のオンプレ環境にあるため、AWS CLIをアクセスキーで制御します。
意図しない動作やリスク軽減のため、専用のユーザーを作成します。
以下の権限を許可したグループを作成、新規ユーザーを作成しそのグループに所属させます。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"ec2:RebootInstances",
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "arn:aws:ec2:*:your_account_id:instance/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeInstanceStatus"
],
"Resource": "*"
}
]
}
aws configure
先ほど作成したユーザーのcredentialをもとにAWS CLIコマンドの設定を行います。
aws configure --profile ap-northeast-1
AWS Access Key ID [None]: your_Access_Key
AWS Secret Access Key [None]: your_Secret_Access_Key
Default region name [None]: ap-northeast-1
Default output format [None]:
#シェルスクリプトの作成
AWS CLIコマンドでEC2インスタンスを操作するシェルスクリプトを作成します。
カレントディレクトリにshフォルダーを新規作成します。
ステータスの取得
vim ./sh/info.sh
aws_discord_bot/info.sh at master · Honahuku/aws_discord_bot
#!/bin/bash
# 変数$1はこのスクリプトの第1引数
# $1の中身がある場合
if [ -n "$1" ]; then
# 変数responseにAWSコマンドの結果を格納
response=$(aws ec2 --profile ap-northeast-1 describe-instances --instance-id $1 --query Reservations[*].Instances[*].State.Name --output text)
# AWSコマンドからの応答に中身がある場合
if [ -n "$response" ]; then
echo $response
# AWSコマンドから文字列が返って来なかった場合
elif [ -z "$STRING" ]; then
echo "null"
fi
# $1の中身が無い場合
elif [ -z "$1" ]; then
echo "Value has not been entered"
fi
パブリックIPの取得
vim ./sh/address.sh
aws_discord_bot/address.sh at master · Honahuku/aws_discord_bot
#!/bin/bash
# 変数$1はこのスクリプトの第1引数
# $1の中身がある場合
if [ -n "$1" ]; then
# 変数responseにAWSコマンドの結果を格納
response=$(aws ec2 --profile ap-northeast-1 describe-instances --instance-id $1 --query 'Reservations[*].Instances[*].PublicIpAddress' --output text)
# AWSコマンドからの応答に中身がある場合
if [ -n "$response" ]; then
echo $response
# AWSコマンドから文字列が返って来なかった場合
elif [ -z "$STRING" ]; then
echo "null"
fi
# $1の中身が無い場合
elif [ -z "$1" ]; then
echo "Value has not been entered"
fi
インスタンス起動
vim ./sh/start.sh
aws_discord_bot/start.sh at master · Honahuku/aws_discord_bot
#!/bin/bash
# 変数$1はこのスクリプトの第1引数
# $1の中身がある場合
if [ -n "$1" ]; then
# 変数responseにAWSコマンドの結果を格納
response=$(aws ec2 --profile ap-northeast-1 start-instances --instance-ids $1 --output text)
# AWSコマンドからの応答に中身がある場合
if [ -n "$response" ]; then
echo $response
# AWSコマンドから文字列が返って来なかった場合
elif [ -z "$STRING" ]; then
echo "null"
fi
# $1の中身が無い場合
elif [ -z "$1" ]; then
echo "Value has not been entered"
fi
インスタンス停止
vim ./sh/stop.sh
aws_discord_bot/stop.sh at master · Honahuku/aws_discord_bot
#!/bin/bash
# 変数$1はこのスクリプトの第1引数
# $1の中身がある場合
if [ -n "$1" ]; then
# 変数responseにAWSコマンドの結果を格納
response=$(aws ec2 --profile ap-northeast-1 stop-instances --instance-ids $1 --output text)
# AWSコマンドからの応答に中身がある場合
if [ -n "$response" ]; then
echo $response
# AWSコマンドから文字列が返って来なかった場合
elif [ -z "$STRING" ]; then
echo "null"
fi
# $1の中身が無い場合
elif [ -z "$1" ]; then
echo "Value has not been entered"
fi
commandsの実装
さきほど作成したcommandsフォルダー内のファイルをすべて削除し、以下のプログラムを実装します。
rm ./commands/*
index.js
vim ./index.js
aws_discord_bot/index.js at master · Honahuku/aws_discord_bot
const fs = require('fs');
const { Client, Intents } = require('discord.js');
const dotenv = require('dotenv');
dotenv.config();
const client = new Client({ intents: [Intents.FLAGS.GUILDS] });
const commands = {};
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
commands[command.data.name] = command;
}
client.once('ready', async () => {
const data = [];
for (const commandName in commands) {
data.push(commands[commandName].data);
}
await client.application.commands.set(data, process.env.SERVER_TOKEN);
console.log('Ready!');
});
client.on('interactionCreate', async (interaction) => {
if (!interaction.isCommand()) {
return;
}
const command = commands[interaction.commandName];
try {
await command.execute(interaction);
}
catch (error) {
console.error(error);
await interaction.reply({
content: 'There was an error while executing this command!',
ephemeral: true,
});
}
});
client.on('ready', () => {
setInterval(() => {
client.user.setActivity({
name: `${client.ws.ping}ms | AWS system`,
});
}, 10000);
});
client.login(process.env.DISCORD_TOKEN);
help
vim ./commands/help.js
aws_discord_bot/help.js at master · Honahuku/aws_discord_bot
// embed用変数宣言
const { MessageEmbed } = require('discord.js');
// embedの定義
// https://scrapbox.io/discordjs-japan/MessageEmbed%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB
const Embed = new MessageEmbed()
.setTitle('aws_discord_bot')
.setDescription('これはDiscord.jsを用いてAWS CLIを操作するBotプログラムです')
.setURL('https://github.com/Honahuku/aws_discord_bot')
.setThumbnail('https://raw.githubusercontent.com/Honahuku/aws_discord_bot/master/img/aws_discord_bot_ico.png')
.setColor('#0099ff')
.addField('利用可能なコマンド', '----')
.addField('info', 'サーバーの状態を取得します')
.addField('start', 'サーバーを起動します')
.addField('stop', 'サーバーを停止します')
.addField('help', 'このヘルプページを表示します')
.addField('----', 'エラーコード')
.addField('"インタラクションに失敗しました"と表示される', '少し待ってから再度コマンドを実行してください');
// スラッシュコマンド用選択肢の定義
module.exports = {
data: {
name: 'help',
description: 'このBotのヘルプページを表示します',
},
// インタラクションの発生、処理を発火
async execute(interaction) {
await interaction.reply({ embeds: [Embed] });
},
};
info
vim ./commands/info.js
aws_discord_bot/info.js at master · Honahuku/aws_discord_bot
// embed用変数宣言
const { MessageEmbed } = require('discord.js');
// シェルコマンドを実行するためにexecSyncの有効化
const execSync = require('child_process').execSync;
// 後から利用する変数の初期化
let status;
let address;
// スラッシュコマンド用選択肢の定義
module.exports = {
data: {
name: 'info',
description: 'サーバー状態を確認します',
options: [{
type: 'STRING',
name: 'server',
description: '確認するサーバーを入力してください',
required: true,
choices: [
{ name: 'dev1', value: 'dev1' },
{ name: 'dev2', value: 'dev2' },
{ name: 'dev3', value: 'dev3' },
],
}],
},
// インタラクションの発生、処理を発火
async execute(interaction) {
// embedの定義
// https://scrapbox.io/discordjs-japan/MessageEmbed%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB
let Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle('Server Info')
.setDescription('サーバーの状態を取得しています');
// 第2引数の値に応じて分岐
if (interaction.options.getString('server') === 'dev1') {
// 既定のembedを一度のみ送信
await interaction.reply({ embeds: [Embed] });
{
// info.shの結果を変数statusに格納
status = execSync(`./sh/info.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// address.shの結果を変数addressに格納
address = execSync(`./sh/address.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
console.log(status);
// 送信用embedの内容編集
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ info server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
// 再定義したembedをeditReplyで編集し内容反映
interaction.editReply({ embeds: [Embed] });
}
}
// 第2引数の値に応じて分岐、以下同様
else if (interaction.options.getString('server') === 'dev2') {
await interaction.reply({ embeds: [Embed] });
{
status = execSync(`./sh/info.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
address = execSync(`./sh/address.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
console.log(status);
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ info server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
interaction.editReply({ embeds: [Embed] });
}
}
else if (interaction.options.getString('server') === 'dev3') {
await interaction.reply({ embeds: [Embed] });
{
status = execSync(`./sh/info.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
address = execSync(`./sh/address.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
console.log(status);
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ info server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
interaction.editReply({ embeds: [Embed] });
}
}
// エラー処理
else {
await interaction.reply('正しいサーバー名を入力してください');
}
},
};
start
vim ./commands/start.js
aws_discord_bot/start.js at master · Honahuku/aws_discord_bot
// embed用変数宣言
const { MessageEmbed } = require('discord.js');
// シェルコマンドを実行するためにexecSyncの有効化
const execSync = require('child_process').execSync;
// 後から利用する変数の初期化
let status;
let address;
// 非同期のdelayを定義
function delay(n) {
return new Promise(function(resolve) {
setTimeout(resolve, n * 1000);
});
}
// スラッシュコマンド用選択肢の定義
module.exports = {
data: {
name: 'start',
description: 'サーバーを起動します',
options: [{
type: 'STRING',
name: 'server',
description: '起動するサーバーを入力してください',
required: true,
choices: [
{ name: 'dev1', value: 'dev1' },
{ name: 'dev2', value: 'dev2' },
{ name: 'dev3', value: 'dev3' },
],
}],
},
// インタラクションの発生、処理を発火
async execute(interaction) {
// embedの定義
// https://scrapbox.io/discordjs-japan/MessageEmbed%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB
let Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle('Server Start')
.setDescription('サーバーを起動しています');
// 第2引数の値に応じて分岐
if (interaction.options.getString('server') === 'dev1') {
// 既定のembedを一度のみ送信
await interaction.reply({ embeds: [Embed] });
// ec2インスタンスの起動処理、start.shを一度のみ実行
execSync(`./sh/start.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// 処理終了まで無限ループ
block: for (; ;) {
// info.shの結果を変数statusに格納
status = execSync(`./sh/info.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// address.shの結果を変数addressに格納
address = execSync(`./sh/address.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// 送信用embedの内容編集
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ start server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
// 再定義したembedをeditReplyで編集し内容反映
interaction.editReply({ embeds: [Embed] });
// statusはバッファ→文字列の変換結果が格納されている。
// 普通に文字列比較を行うと変数statusに制御文字が入っており合致しないため、runningにtoStringを行い制御文字を追加する
// https://qiita.com/masakielastic/items/8eb4bf4efc2310ee7baf#%E6%96%87%E5%AD%97%E5%88%97%E3%81%A8-bufferuint8array-%E3%81%AE%E7%9B%B8%E4%BA%92%E5%A4%89%E6%8F%9B
if (status == Buffer.from('running').toString()) {
// 条件に合致した場合にblock内の処理を終了する
break block;
}
// 以下処理は条件が合致しない場合に実行される
// awsコマンドを連続実行すると負荷がかかるためディレイをもたせる
await delay(1);
}
}
// 第2引数の値に応じて分岐、以下同様
else if (interaction.options.getString('server') === 'dev2') {
execSync(`./sh/start.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
await interaction.reply({ embeds: [Embed] });
block: for (; ;) {
status = execSync(`./sh/info.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
address = execSync(`./sh/address.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ start server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
interaction.editReply({ embeds: [Embed] });
if (status == Buffer.from('running').toString()) {
break block;
}
await delay(1);
}
}
else if (interaction.options.getString('server') === 'dev3') {
execSync(`./sh/start.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
await interaction.reply({ embeds: [Embed] });
block: for (; ;) {
status = execSync(`./sh/info.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
address = execSync(`./sh/address.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ start server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
interaction.editReply({ embeds: [Embed] });
if (status == Buffer.from('running').toString()) {
break block;
}
await delay(1);
}
}
// エラー処理
else {
await interaction.reply('正しいサーバー名を入力してください');
}
},
};
stop
vim ./commands/stop.js
aws_discord_bot/stop.js at master · Honahuku/aws_discord_bot
// embed用変数宣言
const { MessageEmbed } = require('discord.js');
// シェルコマンドを実行するためにexecSyncの有効化
const execSync = require('child_process').execSync;
// 後から利用する変数の初期化
let status;
let address;
// 非同期のdelayを定義
function delay(n) {
return new Promise(function(resolve) {
setTimeout(resolve, n * 1000);
});
}
// スラッシュコマンド用選択肢の定義
module.exports = {
data: {
name: 'stop',
description: 'サーバーを停止します',
options: [{
type: 'STRING',
name: 'server',
description: '停止するサーバーを入力してください',
required: true,
choices: [
{ name: 'dev1', value: 'dev1' },
{ name: 'dev2', value: 'dev2' },
{ name: 'dev3', value: 'dev3' },
],
}],
},
// インタラクションの発生、処理を発火
async execute(interaction) {
// embedの定義
// https://scrapbox.io/discordjs-japan/MessageEmbed%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E5%9F%8B%E3%82%81%E8%BE%BC%E3%81%BF%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB
let Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle('Server Stop')
.setDescription('サーバーを停止しています');
// 第2引数の値に応じて分岐
if (interaction.options.getString('server') === 'dev1') {
// 既定のembedを一度のみ送信
await interaction.reply({ embeds: [Embed] });
// ec2インスタンスの停止処理、stop.shを一度のみ実行
execSync(`./sh/stop.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// 処理終了まで無限ループ
block: for (; ;) {
// info.shの結果を変数statusに格納
status = execSync(`./sh/info.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// address.shの結果を変数addressに格納
address = execSync(`./sh/address.sh ${process.env.DEV1_INSTANCE_ID}`).toString();
// 送信用embedの内容編集
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ stop server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
// 再定義したembedをeditReplyで編集し内容反映
interaction.editReply({ embeds: [Embed] });
// statusはバッファ→文字列の変換結果が格納されている。
// 普通に文字列比較を行うと変数statusに制御文字が入っており合致しないため、runningにtoStringを行い制御文字を追加する
// https://qiita.com/masakielastic/items/8eb4bf4efc2310ee7baf#%E6%96%87%E5%AD%97%E5%88%97%E3%81%A8-bufferuint8array-%E3%81%AE%E7%9B%B8%E4%BA%92%E5%A4%89%E6%8F%9B
if (status == Buffer.from('stopped').toString()) {
// 条件に合致した場合にblock内の処理を終了する
break block;
}
// 以下処理は条件が合致しない場合に実行される
// awsコマンドを連続実行すると負荷がかかるためディレイをもたせる
await delay(1);
}
}
// 第2引数の値に応じて分岐、以下同様
else if (interaction.options.getString('server') === 'dev2') {
await interaction.reply({ embeds: [Embed] });
block: for (; ;) {
execSync(`./sh/stop.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
status = execSync(`./sh/info.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
address = execSync(`./sh/address.sh ${process.env.DEV2_INSTANCE_ID}`).toString();
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ stop server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
interaction.editReply({ embeds: [Embed] });
if (status == Buffer.from('stopped').toString()) {
break block;
}
await delay(1);
}
}
else if (interaction.options.getString('server') === 'dev3') {
await interaction.reply({ embeds: [Embed] });
block: for (; ;) {
execSync(`./sh/stop.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
status = execSync(`./sh/info.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
address = execSync(`./sh/address.sh ${process.env.DEV3_INSTANCE_ID}`).toString();
Embed = new MessageEmbed()
.setColor('#0099ff')
.setTitle(`/ stop server: ${interaction.options.getString('server')}`)
.addField('Status', status)
.addField('Addres', address);
interaction.editReply({ embeds: [Embed] });
if (status == Buffer.from('stopped').toString()) {
break block;
}
await delay(1);
}
}
// エラー処理
else {
await interaction.reply('正しいサーバー名を入力してください');
}
},
};
eslintでエラーチェック
最初に設定したeslintを使ってエラーチェックを行います。
--fix
コマンドで修正できないエラーが出ている場合は、この記事やGitHubリポジトリのコードを参考に修正してください。
eslint --fix ./index.js
eslint --fix commands/help.js
eslint --fix commands/info.js
eslint --fix commands/start.js
eslint --fix commands/stop.js
systemd
毎回サーバーからnode index.js
と実行するのは面倒なのでsystemdを使ってサービス化します。
serviceファイルを作成します。
vim ./aws_discord_bot.service
[Unit]
Description=aws_discord_bot
After=network.target
[Service]
Type = simple
WorkingDirectory=/var/aws_discord_bot
ExecStart=/usr/bin/screen -DmS aws-bot%i /usr/local/bin/node /var/aws_discord_bot/index.js
Restart=always
[Install]
WantedBy=multi-user.target
作成したserviceファイルを登録します。
sudo cp ./aws_discord_bot.service /lib/systemd/system/
sudo systemctl enable aws_discord_bot
sudo systemctl start aws_discord_bot
systemctl status aws_discord_bot
動作検証
これらの動作が確認できたら完成です。
お疲れ様でした。
参考記事
Discord.jsユーザーガイド
nodeのインストール
Ubuntu で Node の最新版/推奨版を使う (n コマンド編) - Qiita
スラッシュコマンド
discord.js でスラッシュコマンド(Slash commands)を使う - Qiita
Embed
Discord.jsでembed (埋め込みメッセージ) を扱う - Qiita
MessageEmbedを使って埋め込みを送信するサンプル - Discord.js Japan User Group
スラッシュコマンドでembed
How to make a slash commands bot with Discord.js V13 - DEV Community
AWS_CLIとIAM
AWS EC2の起動と停止のみが可能なIAMポリシーを作成してIAMポリシーの設定方法を理解する | Tech Note Meeting
execSync
Node.jsからシェルコマンドを実行する - BppLOG
delay
JavaScript で X 秒を待つ | Delft スタック
バッファーから文字列への変換
JavaScript、Node.js で文字列とバイト列の相互変換 - Qiita
-
ちょうど.jsを始めたところだったので、勉強がてらという理由もあります ↩