ClientX/_opt/botUtils.js
2025-05-03 20:01:29 +00:00

163 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 clients 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');
}