ClientX/_opt/botUtils.js

154 lines
6.6 KiB
JavaScript
Raw Normal View History

2025-05-03 19:43:14 +00:00
import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js';
2025-05-01 22:58:30 +00:00
/**
* 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')
2025-05-02 16:45:36 +00:00
.setDescription('Gracefully shutdown the bot (Owner only)')
2025-05-01 22:58:30 +00:00
// 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(
2025-05-02 16:45:36 +00:00
`[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}`
2025-05-01 22:58:30 +00:00
);
// Destroy Discord client and exit process
try {
await client.destroy();
} catch (err) {
2025-05-02 16:45:36 +00:00
client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`);
2025-05-01 22:58:30 +00:00
}
process.exit(exitCode);
}
},
// /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()) : [];
2025-05-03 19:43:14 +00:00
// Build embed for status
// Determine if gitUtils module is loaded
const gitLoaded = client.modules?.has('gitUtils');
let branch, build, statusBlock;
if (gitLoaded) {
const git = client.modules.get('gitUtils');
try {
branch = await git.getBranch();
build = await git.getShortHash();
const 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
statusBlock = statusRaw
? '```\n ' + statusRaw + '\n```'
: '```\n (clean)\n```';
} catch {
branch = 'error';
build = 'error';
// Represent error status in code fence
statusBlock = '```\n (error)\n```';
}
2025-05-03 17:27:21 +00:00
}
2025-05-03 19:43:14 +00:00
// 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 });
2025-05-03 17:27:21 +00:00
}
2025-05-03 19:43:14 +00:00
// 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}`);
2025-05-03 17:27:21 +00:00
}
2025-05-01 22:58:30 +00:00
}
];
2025-05-03 19:43:14 +00:00
// Module loaded logging
2025-05-01 22:58:30 +00:00
export async function init(client, clientConfig) {
2025-05-02 16:45:36 +00:00
client.logger.info('[module:botUtils] Module loaded');
2025-05-01 22:58:30 +00:00
}