From d6d0ca1db6ddc7e1173418c64e27b26c8ac00a95 Mon Sep 17 00:00:00 2001 From: jrmyr Date: Thu, 1 May 2025 22:58:30 +0000 Subject: [PATCH] Added botUtils --- _opt/botUtils.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++ config.js | 1 + 2 files changed, 123 insertions(+) create mode 100644 _opt/botUtils.js diff --git a/_opt/botUtils.js b/_opt/botUtils.js new file mode 100644 index 0000000..edcabfe --- /dev/null +++ b/_opt/botUtils.js @@ -0,0 +1,122 @@ +import { SlashCommandBuilder, PermissionFlagsBits } 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( + `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(`Error during client.destroy(): ${err}`); + } + 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()) : []; + const lines = [ + `Client ID : ${client.config.id}`, + `Uptime : ${uptime}`, + `Memory : ${memoryInfo}`, + `CPU Usage : ${cpuInfo}`, + `Node.js : ${nodeVersion}`, + `Platform : ${platform}`, + `Guilds : ${guildCount}`, + `Users : ${userCount}`, + `Commands : ${commandCount}`, + `Modules : ${loadedModules.length > 0 ? loadedModules.join(', ') : 'None'}` + ]; + await interaction.editReply({ content: '```\n' + lines.join('\n') + '\n```' }); + } + } +]; + +// Remove statusall command: not needed + +// Enhance status command to list loaded modules +// (nothing else follows) +// Store all initialized clients for statusall +const botUtilsClients = []; + +/** + * Optional init hook; logs when the module is loaded and tracks clients + * @param {import('discord.js').Client} client + * @param {Object} clientConfig + */ +export async function init(client, clientConfig) { + client.logger.info('botUtils module loaded'); + // Track this client instance and its config + botUtilsClients.push({ client, clientConfig }); +} \ No newline at end of file diff --git a/config.js b/config.js index 6a1ebba..f9c95e9 100644 --- a/config.js +++ b/config.js @@ -67,6 +67,7 @@ export default { }, modules: [ + 'botUtils', 'pbUtils', 'responses', 'responsesQuery', -- 2.39.5