163 lines
7.1 KiB
JavaScript
163 lines
7.1 KiB
JavaScript
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
|
||
|
||
/**
|
||
* botUtils module - provides administrative bot control commands
|
||
* Currently implements an owner-only exit command for graceful shutdown.
|
||
*/
|
||
// Define slash commands
|
||
export const commands = [
|
||
{
|
||
data: new SlashCommandBuilder()
|
||
.setName('exit')
|
||
.setDescription('Gracefully shutdown the bot (Owner only)')
|
||
// Restrict to server administrators by default
|
||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||
.setDMPermission(false)
|
||
.addIntegerOption(option =>
|
||
option
|
||
.setName('code')
|
||
.setDescription('Exit code to use (default 0)')
|
||
.setRequired(false)
|
||
),
|
||
/**
|
||
* Execute the exit command: only the configured owner can invoke.
|
||
* @param {import('discord.js').CommandInteraction} interaction
|
||
* @param {import('discord.js').Client} client
|
||
*/
|
||
async execute(interaction, client) {
|
||
const ownerId = client.config.owner;
|
||
// Check invoking user is the bot owner
|
||
if (interaction.user.id !== String(ownerId)) {
|
||
return interaction.reply({ content: 'Only the bot owner can shutdown the bot.', ephemeral: true });
|
||
}
|
||
// Determine desired exit code (default 0)
|
||
const exitCode = interaction.options.getInteger('code') ?? 0;
|
||
// Validate exit code bounds
|
||
if (exitCode < 0 || exitCode > 254) {
|
||
return interaction.reply({ content: 'Exit code must be between 0 and 254 inclusive.', ephemeral: true });
|
||
}
|
||
// Acknowledge before shutting down
|
||
await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, ephemeral: true });
|
||
client.logger.info(
|
||
`[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}`
|
||
);
|
||
// Destroy Discord client and exit process
|
||
try {
|
||
await client.destroy();
|
||
} catch (err) {
|
||
client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`);
|
||
}
|
||
process.exit(exitCode);
|
||
}
|
||
},
|
||
/**
|
||
* Slash command `/status` (Administrator only):
|
||
* Shows this bot client’s status including CPU, memory, environment,
|
||
* uptime, module list, and entity counts. Optionally displays Git info
|
||
* (Git Reference and Git Status) when the gitUtils module is loaded.
|
||
* @param {import('discord.js').CommandInteraction} interaction
|
||
* @param {import('discord.js').Client} client
|
||
*/
|
||
// /status: admin-only, shows current client info
|
||
{
|
||
data: new SlashCommandBuilder()
|
||
.setName('status')
|
||
.setDescription('Show this bot client status and process info')
|
||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||
.setDMPermission(false)
|
||
.addBooleanOption(option =>
|
||
option
|
||
.setName('ephemeral')
|
||
.setDescription('Whether the response should be ephemeral')
|
||
.setRequired(false)
|
||
),
|
||
async execute(interaction, client) {
|
||
// Determine if response should be ephemeral (default true)
|
||
const ephemeral = interaction.options.getBoolean('ephemeral') ?? true;
|
||
await interaction.deferReply({ ephemeral });
|
||
// Process metrics
|
||
const uptimeSec = process.uptime();
|
||
const hours = Math.floor(uptimeSec / 3600);
|
||
const minutes = Math.floor((uptimeSec % 3600) / 60);
|
||
const seconds = Math.floor(uptimeSec % 60);
|
||
const uptime = `${hours}h ${minutes}m ${seconds}s`;
|
||
const mem = process.memoryUsage();
|
||
const toMB = bytes => (bytes / 1024 / 1024).toFixed(2);
|
||
const memoryInfo = `RSS: ${toMB(mem.rss)} MB, Heap: ${toMB(mem.heapUsed)}/${toMB(mem.heapTotal)} MB`;
|
||
const cpu = process.cpuUsage();
|
||
const cpuInfo = `User: ${(cpu.user / 1000).toFixed(2)} ms, System: ${(cpu.system / 1000).toFixed(2)} ms`;
|
||
const nodeVersion = process.version;
|
||
const platform = `${process.platform} ${process.arch}`;
|
||
// Client-specific stats
|
||
const guildCount = client.guilds.cache.size;
|
||
const userCount = client.guilds.cache.reduce((sum, g) => sum + (g.memberCount || 0), 0);
|
||
const commandCount = client.commands.size;
|
||
// List of loaded optional modules
|
||
const loadedModules = client.modules ? Array.from(client.modules.keys()) : [];
|
||
// Build embed for status
|
||
// Determine if gitUtils module is loaded
|
||
const gitLoaded = client.modules?.has('gitUtils');
|
||
let branch, build, statusRaw, statusBlock;
|
||
if (gitLoaded) {
|
||
const git = client.modules.get('gitUtils');
|
||
try {
|
||
branch = await git.getBranch();
|
||
build = await git.getShortHash();
|
||
statusRaw = await git.getStatusShort();
|
||
// Format status as fenced code block using template literals
|
||
// Normalize each line with a leading space in a code fence
|
||
// Prefix raw status output with a single space
|
||
// Prefix raw status output with a space, and only if non-empty
|
||
if (statusRaw) {
|
||
statusBlock = '```\n ' + statusRaw + '\n```';
|
||
}
|
||
} catch {
|
||
branch = 'error';
|
||
build = 'error';
|
||
// Represent error status in code fence
|
||
statusBlock = '```\n (error)\n```';
|
||
}
|
||
}
|
||
// Prepare module list as bullet points
|
||
const moduleList = loadedModules.length > 0
|
||
? loadedModules.map(m => `• ${m}`).join('\n')
|
||
: 'None';
|
||
// Assemble fields
|
||
const fields = [];
|
||
// Client identification
|
||
fields.push({ name: 'Client', value: client.config.id, inline: false });
|
||
// Performance metrics
|
||
fields.push({ name: 'CPU Usage', value: cpuInfo, inline: false });
|
||
fields.push({ name: 'Memory', value: memoryInfo, inline: false });
|
||
// Environment
|
||
fields.push({ name: 'Node.js', value: nodeVersion, inline: true });
|
||
fields.push({ name: 'Platform', value: platform, inline: true });
|
||
// Uptime
|
||
fields.push({ name: 'Uptime', value: uptime, inline: true });
|
||
// Loaded modules
|
||
fields.push({ name: 'Modules', value: moduleList, inline: false });
|
||
// Entity counts
|
||
fields.push({ name: 'Commands', value: commandCount.toString(), inline: true });
|
||
fields.push({ name: 'Guilds', value: guildCount.toString(), inline: true });
|
||
fields.push({ name: 'Users', value: userCount.toString(), inline: true });
|
||
// Git reference and status if available
|
||
if (gitLoaded) {
|
||
fields.push({ name: 'Git Reference', value: `${branch}/${build}`, inline: false });
|
||
fields.push({ name: 'Git Status', value: statusBlock, inline: false });
|
||
}
|
||
// Create embed
|
||
const embed = new EmbedBuilder()
|
||
.setAuthor({ name: 'ClientX', iconURL: client.user.displayAvatarURL() })
|
||
.setThumbnail(client.user.displayAvatarURL())
|
||
.addFields(fields)
|
||
.setTimestamp();
|
||
await interaction.editReply({ embeds: [embed] });
|
||
client.logger.info(`[cmd:status] Returned status embed for client ${client.config.id}`);
|
||
}
|
||
}
|
||
];
|
||
|
||
// Module loaded logging
|
||
export async function init(client, clientConfig) {
|
||
client.logger.info('[module:botUtils] Module loaded');
|
||
} |