============================= onCommandsInteraction triggered by event handler on interactionCreate event ========================================== import { GUILD_IDS_OWNED, USER_IDS_DEVELOPERS, } from '#config'; import getLocalCommands from '#services/commandManager/getLocalCommands.js'; import { sendPlainMessage } from '#utils/globalUtils/sendMessageUtils.js'; import { createLogger } from '#utils/globalUtils/logger.js'; const logger = createLogger('CommandInteractionHandler'); export default async function onCommandsInteraction(client, interaction) { if (!interaction.isChatInputCommand()) return; try { // ❌ No deferred/reply here — command-level ACK only if (!interaction.guild) { logger.warn(`DM command attempt by ${interaction.user.tag} (${interaction.user.id})`); return await sendPlainMessage(interaction, 'This bot only supports slash commands inside servers.'); } const localCommands = new Map( (await getLocalCommands()).map(cmd => [cmd.name, cmd]) ); const commandName = interaction.commandName; if (!commandName) { logger.warn(`Null commandName in guild ${interaction.guildId}`); return await sendPlainMessage(interaction, 'Invalid command interaction received.'); } const command = localCommands.get(commandName); if (!command) { return await sendPlainMessage(interaction, 'Command not found.'); } if (command.devOnly && !USER_IDS_DEVELOPERS.includes(interaction.user.id)) { return await sendPlainMessage(interaction, 'This command is restricted.'); } if (command.ownedGuildsOnly && !GUILD_IDS_OWNED.includes(interaction.guild.id)) { return await sendPlainMessage(interaction, 'This command can only be used here.'); } const member = interaction.member; const botMember = interaction.guild.members.me; const permError = checkPermissions(member?.permissions, command.permissionsRequired, 'user') || checkPermissions(botMember?.permissions, command.botPermissions, 'bot'); if (permError) { return await sendPlainMessage(interaction, permError); } // ✅ Pass to command handler — it must ack! await command.execute(client, interaction); } catch (err) { logger.error(`Error executing ${interaction.commandName} in "${interaction.guild?.name ?? 'Unknown'}" (${interaction.guildId}):`, err); await sendPlainMessage(interaction, 'There was an error executing the command.'); } } function checkPermissions(permBitfield, required = [], type) { if (!permBitfield || !required.length) return null; for (const perm of required) { if (!permBitfield.has(perm)) { return type === 'user' ? 'You lack required permissions.' : 'I lack required permissions.'; } } return null; } =============================================== leaderboard sub cmd code ========================================== import { EmbedBuilder } from 'discord.js'; import GraveyardUserState from '#models/GraveyardUserState.js'; import { sendEmbedMessage, sendPlainMessage } from '#utils/globalUtils/sendMessageUtils.js'; import { getOrFetchGuild } from '#utils/globalUtils/getOrFetchGuild.js'; import { createLogger } from '#utils/globalUtils/logger.js'; import { COLOR_EMBEDS } from '#config'; const logger = createLogger('GraveyardLeaderboard'); /** * Converts milliseconds to hours (1 decimal place). * @param {number} ms * @returns {string} */ const formatMsToHrs = (ms) => (ms / 3600000).toFixed(1); /** * Formats a mention if the user exists, or shows "Unknown User" with their ID. * @param {import('discord.js').Client} client * @param {string} userId * @returns {Promise} */ const formatMention = async (client, userId) => { try { await client.users.fetch(userId); return `<@${userId}>`; } catch { return `Unknown User (\`${userId}\`)`; } }; /** * Formats a leaderboard list for display. * @param {Array} entries * @param {(entry: any) => string|number} getValFn * @param {string} unitLabel * @param {import('discord.js').Client} client * @returns {Promise} */ const formatList = async (entries, getValFn, unitLabel, client) => { return Promise.all( entries.map(async (entry, idx) => { const mention = await formatMention(client, entry.userId); const val = getValFn(entry); return `**#${idx + 1}** - ${mention}: ${val}${unitLabel}`; }) ); }; /** * Sends the graveyard leaderboard (Gravekeeper & Lingering Soul) to the user. * * @param {import('discord.js').Client} client - The Discord client * @param {import('discord.js').ChatInputCommandInteraction} interaction - The interaction object */ export async function leaderboardGraveyard(client, interaction) { await interaction.deferReply(); const guildId = interaction.guildId; let guild = interaction.guild; if (!guild || !guild.available) { guild = await getOrFetchGuild(client, guildId); if (!guild) { logger.warn(`Could not resolve guild ${guildId} for leaderboard command.`); return sendPlainMessage(interaction, '⚠️ Could not load guild info to display the leaderboard.'); } } try { const selectFields = 'userId stats'; const [gkByTime, gkByClaims, lsByTime, lsByClaims] = await Promise.all([ GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.totalGravekeeperTime': -1 }).limit(10).lean(), GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.gravekeeperClaims': -1 }).limit(10).lean(), GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.totalLingeringTime': -1 }).limit(10).lean(), GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.lingeringSoulClaims': -1 }).limit(10).lean(), ]); const gkTimeList = await formatList(gkByTime, (e) => formatMsToHrs(e.stats.totalGravekeeperTime), 'h', client); const gkClaimList = await formatList(gkByClaims, (e) => e.stats.gravekeeperClaims, 'x', client); const lsTimeList = await formatList(lsByTime, (e) => formatMsToHrs(e.stats.totalLingeringTime), 'h', client); const lsClaimList = await formatList(lsByClaims, (e) => e.stats.lingeringSoulClaims, 'x', client); const gkRole = guild.roles.cache.find((r) => r.name.toLowerCase().includes('gravekeeper')); const lsRole = guild.roles.cache.find((r) => r.name.toLowerCase().includes('lingering')); const gkColor = gkRole?.color || COLOR_EMBEDS.PRIMARY; const lsColor = lsRole?.color || COLOR_EMBEDS.INFO; const gkEmbed = new EmbedBuilder() .setColor(gkColor) .setTitle('🏆 Gravekeeper Leaderboard') .setDescription( `**⏱️ Time Held:**\n${gkTimeList.join('\n') || '*No data*'}\n\n` + `**🏅 Claims:**\n${gkClaimList.join('\n') || '*No data*'}` ); const lsEmbed = new EmbedBuilder() .setColor(lsColor) .setTitle('👻 Lingering Souls Leaderboard') .setDescription( `**⏱️ Time Lingering:**\n${lsTimeList.join('\n') || '*No data*'}\n\n` + `**🏅 Claims:**\n${lsClaimList.join('\n') || '*No data*'}` ) .setFooter({ text: guild.name, iconURL: guild.iconURL() }) .setTimestamp(); return sendEmbedMessage(interaction, [gkEmbed, lsEmbed], [], false); } catch (err) { logger.error(`Failed to send graveyard leaderboard for guild ${guild?.name ?? guildId} (${guildId}):`, err); return sendPlainMessage(interaction, '❌ Something went wrong while generating the leaderboard.'); } }