From 9ffba4f2da43946f70b22285b41a4c73b1512d44 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:30:01 -0500 Subject: [PATCH 1/7] Adding in files --- src/commands/admin/admin.ts | 534 ++++++++++++++++++++++++---------- src/commands/help/helpText.ts | 17 +- 2 files changed, 396 insertions(+), 155 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 1cac921f..8c1c2dfb 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -1,185 +1,419 @@ -// src/commands/admin/admin.ts import { SlashCommandBuilder, - EmbedBuilder, PermissionFlagsBits, type ChatInputCommandInteraction, + type GuildMember, + MessageFlags, } from "discord.js"; import { logger } from "../../utils/logger.js"; -import { getDb } from "../../services/database/db.js"; -import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; -import { EmbedColors } from "../../utils/colors.js"; +import { getDb } from "../../db/index.js"; +/** + * /admin + * + * Role-based moderation commands: timeout, kick, ban + */ export const data = new SlashCommandBuilder() .setName("admin") - .setDescription("Admin commands") - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + .setDescription("Moderation commands (role-based)") .addSubcommand((sub) => sub - .setName("stats") - .setDescription("Show bot statistics (uptime, database, commands)"), + .setName("timeout") + .setDescription("Timeout a user") + .addUserOption((opt) => + opt + .setName("user") + .setDescription("User to timeout") + .setRequired(true) + ) + .addIntegerOption((opt) => + opt + .setName("duration") + .setDescription("Duration in minutes") + .setRequired(true) + .setMinValue(1) + .setMaxValue(40320) // 28 days max + ) + .addStringOption((opt) => + opt + .setName("reason") + .setDescription("Reason for timeout") + .setRequired(false) + ) ) .addSubcommand((sub) => - sub.setName("health").setDescription("Check bot and service health"), - ); + sub + .setName("kick") + .setDescription("Kick a user from the server") + .addUserOption((opt) => + opt + .setName("user") + .setDescription("User to kick") + .setRequired(true) + ) + .addStringOption((opt) => + opt + .setName("reason") + .setDescription("Reason for kick") + .setRequired(false) + ) + ) + .addSubcommand((sub) => + sub + .setName("ban") + .setDescription("Ban a user from the server") + .addUserOption((opt) => + opt + .setName("user") + .setDescription("User to ban") + .setRequired(true) + ) + .addStringOption((opt) => + opt + .setName("reason") + .setDescription("Reason for ban") + .setRequired(false) + ) + .addIntegerOption((opt) => + opt + .setName("delete_days") + .setDescription("Days of messages to delete (0-7)") + .setRequired(false) + .setMinValue(0) + .setMaxValue(7) + ) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild); export async function execute(interaction: ChatInputCommandInteraction): Promise { - const subcommand = interaction.options.getSubcommand(); + try { + // Must be in a guild + if (!interaction.inGuild()) { + await interaction.reply({ + content: "This command can only be used in a server.", + flags: MessageFlags.Ephemeral, + }); + return; + } - await interaction.deferReply(); + // Check if user has the required role + const hasPermission = await checkModeratorRole(interaction); + if (!hasPermission) { + await interaction.reply({ + content: "❌ You don't have permission to use moderation commands.\n" + + "Ask an admin to set up moderator roles with `/config moderator-role`.", + flags: MessageFlags.Ephemeral, + }); + return; + } - try { - if (subcommand === "stats") { - await handleStats(interaction); - } else if (subcommand === "health") { - await handleHealth(interaction); - } else { - await interaction.editReply("Unknown subcommand"); + const subcommand = interaction.options.getSubcommand(); + + switch (subcommand) { + case "timeout": + await handleTimeout(interaction); + break; + case "kick": + await handleKick(interaction); + break; + case "ban": + await handleBan(interaction); + break; + default: + await interaction.reply({ + content: "Unknown subcommand.", + flags: MessageFlags.Ephemeral, + }); + } + } catch (err) { + logger.error( + { err, command: "admin", userId: interaction.user.id }, + "[admin] command failed" + ); + + try { + if (!interaction.replied && !interaction.deferred) { + await interaction.reply({ + content: "Something went wrong. Please try again later.", + flags: MessageFlags.Ephemeral, + }); + } + } catch (replyErr) { + logger.error({ err: replyErr }, "[admin] failed to send error reply"); } - } catch (error) { - logger.error({ error, subcommand }, "[admin] Command failed"); - await interaction.editReply("❌ Something went wrong"); } } -async function handleStats(interaction: ChatInputCommandInteraction): Promise { - const db = getDb(); - - // Get database stats - const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { - count: number; - }; - - const coinFlipCount = db.prepare("SELECT COUNT(*) as count FROM coin_flips").get() as { - count: number; - }; - - // Get fun command usage - const funUsage = await getFunUsageSnapshot(); - const totalCommands = Object.values(funUsage.totalsByCommand).reduce( - (sum, count) => sum + count, - 0, - ); - - // Calculate uptime - const uptimeSeconds = process.uptime(); - const uptimeDays = Math.floor(uptimeSeconds / 86400); - const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600); - const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60); - - // Memory usage - const memUsage = process.memoryUsage(); - const memUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024); - const memTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024); - - const embed = new EmbedBuilder() - .setTitle("πŸ€– Bot Statistics") - .setColor(EmbedColors.Info) - .addFields( - { - name: "⏱️ Uptime", - value: `${uptimeDays}d ${uptimeHours}h ${uptimeMinutes}m`, - inline: true, - }, - { - name: "πŸ’Ύ Memory", - value: `${memUsedMB}MB / ${memTotalMB}MB`, - inline: true, - }, - { - name: "πŸ“Š Total Commands", - value: totalCommands.toString(), - inline: true, - }, - { - name: "🎭 Jokes", - value: jokeCount.count.toString(), - inline: true, - }, - { - name: "πŸͺ™ Coin Flips", - value: coinFlipCount.count.toString(), - inline: true, - }, - { - name: "πŸ‘₯ Unique Users", - value: Object.keys(funUsage.totalsByUser).length.toString(), - inline: true, - }, - ) - .setFooter({ text: `Node ${process.version}` }) - .setTimestamp(); +/** + * Check if user has moderator role + */ +async function checkModeratorRole(interaction: ChatInputCommandInteraction): Promise { + if (!interaction.inGuild() || !interaction.member) { + return false; + } + + // Admins always have permission + if (interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { + return true; + } - await interaction.editReply({ embeds: [embed] }); + try { + const db = getDb(); + const guildId = interaction.guildId!; - logger.info({ userId: interaction.user.id }, "Admin viewed bot statistics"); + // Check for moderator roles in database + const roles = db + .prepare( + `SELECT role_id FROM moderator_roles WHERE guild_id = ?` + ) + .all(guildId) as Array<{ role_id: string }>; + + if (roles.length === 0) { + // No moderator roles configured - only admins can use + return false; + } + + const member = interaction.member as GuildMember; + const memberRoles = member.roles.cache; + + // Check if user has any of the moderator roles + return roles.some((r) => memberRoles.has(r.role_id)); + } catch (err) { + logger.error({ err }, "[admin] failed to check moderator role"); + return false; + } } -async function handleHealth(interaction: ChatInputCommandInteraction): Promise { - const checks: { name: string; status: string; details?: string }[] = []; +/** + * Handle timeout subcommand + */ +async function handleTimeout(interaction: ChatInputCommandInteraction): Promise { + const targetUser = interaction.options.getUser("user", true); + const duration = interaction.options.getInteger("duration", true); + const reason = interaction.options.getString("reason") ?? "No reason provided"; + + if (!interaction.guild) { + await interaction.reply({ + content: "This command can only be used in a server.", + flags: MessageFlags.Ephemeral, + }); + return; + } - // Check database try { - const db = getDb(); - db.prepare("SELECT 1").get(); - checks.push({ name: "Database", status: "βœ… Healthy" }); - } catch (error) { - checks.push({ - name: "Database", - status: "❌ Error", - details: error instanceof Error ? error.message : "Unknown error", + const member = await interaction.guild.members.fetch(targetUser.id); + + // Can't timeout bots or self + if (member.user.bot) { + await interaction.reply({ + content: "❌ Cannot timeout bots.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (member.id === interaction.user.id) { + await interaction.reply({ + content: "❌ You cannot timeout yourself.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Check role hierarchy + const executor = interaction.member as GuildMember; + if (member.roles.highest.position >= executor.roles.highest.position) { + await interaction.reply({ + content: "❌ You cannot timeout someone with an equal or higher role.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Apply timeout + const durationMs = duration * 60 * 1000; + await member.timeout(durationMs, reason); + + await interaction.reply({ + content: `βœ… ${targetUser.tag} has been timed out for ${duration} minute(s).\n**Reason:** ${reason}`, + }); + + logger.info( + { + moderator: interaction.user.tag, + target: targetUser.tag, + duration, + reason, + }, + "[admin] User timed out" + ); + } catch (err) { + logger.error({ err, targetUser: targetUser.id }, "[admin] timeout failed"); + await interaction.reply({ + content: "❌ Failed to timeout user. Check my permissions and role position.", + flags: MessageFlags.Ephemeral, }); } +} - // Check environment variables - const requiredEnvVars = [ - "DISCORD_TOKEN", - "DISCORD_APP_ID", - "GITHUB_TOKEN", - "GITHUB_REPO_OWNER", - "GITHUB_REPO_NAME", - ]; - - const missingVars = requiredEnvVars.filter((v) => !process.env[v]); - if (missingVars.length === 0) { - checks.push({ name: "Environment", status: "βœ… All vars set" }); - } else { - checks.push({ - name: "Environment", - status: "⚠️ Missing vars", - details: missingVars.join(", "), +/** + * Handle kick subcommand + */ +async function handleKick(interaction: ChatInputCommandInteraction): Promise { + const targetUser = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason") ?? "No reason provided"; + + if (!interaction.guild) { + await interaction.reply({ + content: "This command can only be used in a server.", + flags: MessageFlags.Ephemeral, }); + return; } - // Check API keys - const optionalKeys = [ - { name: "Anthropic API", key: "ANTHROPIC_API_KEY" }, - { name: "Weather API", key: "WEATHER_API_KEY" }, - ]; - - optionalKeys.forEach(({ name, key }) => { - if (process.env[key]) { - checks.push({ name, status: "βœ… Configured" }); - } else { - checks.push({ name, status: "⚠️ Not configured" }); + try { + const member = await interaction.guild.members.fetch(targetUser.id); + + // Can't kick bots or self + if (member.user.bot) { + await interaction.reply({ + content: "❌ Cannot kick bots.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (member.id === interaction.user.id) { + await interaction.reply({ + content: "❌ You cannot kick yourself.", + flags: MessageFlags.Ephemeral, + }); + return; } - }); - - const embed = new EmbedBuilder() - .setTitle("πŸ₯ Health Check") - .setColor(EmbedColors.Info) - .setDescription( - checks - .map((c) => - c.details - ? `**${c.name}:** ${c.status}\n ${c.details}` - : `**${c.name}:** ${c.status}`, - ) - .join("\n\n"), - ) - .setTimestamp(); - - await interaction.editReply({ embeds: [embed] }); - - logger.info({ userId: interaction.user.id }, "Admin viewed health check"); + + // Check role hierarchy + const executor = interaction.member as GuildMember; + if (member.roles.highest.position >= executor.roles.highest.position) { + await interaction.reply({ + content: "❌ You cannot kick someone with an equal or higher role.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Check if member is kickable + if (!member.kickable) { + await interaction.reply({ + content: "❌ I don't have permission to kick this user.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Kick user + await member.kick(reason); + + await interaction.reply({ + content: `βœ… ${targetUser.tag} has been kicked.\n**Reason:** ${reason}`, + }); + + logger.info( + { + moderator: interaction.user.tag, + target: targetUser.tag, + reason, + }, + "[admin] User kicked" + ); + } catch (err) { + logger.error({ err, targetUser: targetUser.id }, "[admin] kick failed"); + await interaction.reply({ + content: "❌ Failed to kick user. Check my permissions and role position.", + flags: MessageFlags.Ephemeral, + }); + } } + +/** + * Handle ban subcommand + */ +async function handleBan(interaction: ChatInputCommandInteraction): Promise { + const targetUser = interaction.options.getUser("user", true); + const reason = interaction.options.getString("reason") ?? "No reason provided"; + const deleteDays = interaction.options.getInteger("delete_days") ?? 0; + + if (!interaction.guild) { + await interaction.reply({ + content: "This command can only be used in a server.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + try { + const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); + + // Can't ban self + if (targetUser.id === interaction.user.id) { + await interaction.reply({ + content: "❌ You cannot ban yourself.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + // Check role hierarchy if member is in server + if (member) { + if (member.user.bot) { + await interaction.reply({ + content: "❌ Cannot ban bots.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + const executor = interaction.member as GuildMember; + if (member.roles.highest.position >= executor.roles.highest.position) { + await interaction.reply({ + content: "❌ You cannot ban someone with an equal or higher role.", + flags: MessageFlags.Ephemeral, + }); + return; + } + + if (!member.bannable) { + await interaction.reply({ + content: "❌ I don't have permission to ban this user.", + flags: MessageFlags.Ephemeral, + }); + return; + } + } + + // Ban user + await interaction.guild.members.ban(targetUser.id, { + reason, + deleteMessageSeconds: deleteDays * 24 * 60 * 60, + }); + + await interaction.reply({ + content: `βœ… ${targetUser.tag} has been banned.\n**Reason:** ${reason}${deleteDays > 0 ? `\n**Messages deleted:** Last ${deleteDays} day(s)` : ""}`, + }); + + logger.info( + { + moderator: interaction.user.tag, + target: targetUser.tag, + reason, + deleteDays, + }, + "[admin] User banned" + ); + } catch (err) { + logger.error({ err, targetUser: targetUser.id }, "[admin] ban failed"); + await interaction.reply({ + content: "❌ Failed to ban user. Check my permissions and role position.", + flags: MessageFlags.Ephemeral, + }); + } +} \ No newline at end of file diff --git a/src/commands/help/helpText.ts b/src/commands/help/helpText.ts index e036b8fa..8a3c1431 100644 --- a/src/commands/help/helpText.ts +++ b/src/commands/help/helpText.ts @@ -71,7 +71,7 @@ function buildOverviewHelp(args: { isAdmin: boolean }): string { "`/gh status` Check GitHub integration status", "`/summary` Summarize recent messages", "", - "If new commands don’t show up, an admin may need to run the register script.", + "If new commands don't show up, an admin may need to run the register script.", ].join("\n"); } @@ -87,7 +87,7 @@ function buildFunHelp(): string { "`/fun dice` Roll dice", "`/fun coinflip` Flip a coin", "`/fun poll` Create a poll", - "`/fun weather` Today’s weather", + "`/fun weather` Today's weather", "`/fun weather7` 7-day forecast", "`/fun leaderboard` Show fun usage stats", ].join("\n"); @@ -142,10 +142,17 @@ function buildAdminHelp(args: { isAdmin: boolean }): string { return [ "**Help: Admin**", "", - "`/config welcome-channel set`", - "`/config welcome-channel clear`", + "**Server Configuration**", + "`/config welcome-channel set` Set welcome channel", + "`/config welcome-channel clear` Remove welcome channel", + "", + "**Role-Based Moderation**", + "`/admin timeout` Timeout a user (requires role)", + "`/admin kick` Kick a user (requires role)", + "`/admin ban` Ban a user (requires role)", "", "You need Manage Server permissions to run these.", + "Moderation commands require specific roles set by server config.", ].join("\n"); } @@ -196,4 +203,4 @@ function titleCase(s: string): string { .filter(Boolean) .map((w) => w[0].toUpperCase() + w.slice(1)) .join(" "); -} +} \ No newline at end of file From eca94ae7a975910f8a00143cf3d39e9449924ab1 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:30:06 -0500 Subject: [PATCH 2/7] style: auto-format with Prettier [skip-precheck] --- src/commands/admin/admin.ts | 74 ++++++++++++++--------------------- src/commands/help/helpText.ts | 2 +- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 8c1c2dfb..9eafdd7d 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -21,58 +21,41 @@ export const data = new SlashCommandBuilder() .setName("timeout") .setDescription("Timeout a user") .addUserOption((opt) => - opt - .setName("user") - .setDescription("User to timeout") - .setRequired(true) + opt.setName("user").setDescription("User to timeout").setRequired(true), ) - .addIntegerOption((opt) => - opt - .setName("duration") - .setDescription("Duration in minutes") - .setRequired(true) - .setMinValue(1) - .setMaxValue(40320) // 28 days max + .addIntegerOption( + (opt) => + opt + .setName("duration") + .setDescription("Duration in minutes") + .setRequired(true) + .setMinValue(1) + .setMaxValue(40320), // 28 days max ) .addStringOption((opt) => - opt - .setName("reason") - .setDescription("Reason for timeout") - .setRequired(false) - ) + opt.setName("reason").setDescription("Reason for timeout").setRequired(false), + ), ) .addSubcommand((sub) => sub .setName("kick") .setDescription("Kick a user from the server") .addUserOption((opt) => - opt - .setName("user") - .setDescription("User to kick") - .setRequired(true) + opt.setName("user").setDescription("User to kick").setRequired(true), ) .addStringOption((opt) => - opt - .setName("reason") - .setDescription("Reason for kick") - .setRequired(false) - ) + opt.setName("reason").setDescription("Reason for kick").setRequired(false), + ), ) .addSubcommand((sub) => sub .setName("ban") .setDescription("Ban a user from the server") .addUserOption((opt) => - opt - .setName("user") - .setDescription("User to ban") - .setRequired(true) + opt.setName("user").setDescription("User to ban").setRequired(true), ) .addStringOption((opt) => - opt - .setName("reason") - .setDescription("Reason for ban") - .setRequired(false) + opt.setName("reason").setDescription("Reason for ban").setRequired(false), ) .addIntegerOption((opt) => opt @@ -80,8 +63,8 @@ export const data = new SlashCommandBuilder() .setDescription("Days of messages to delete (0-7)") .setRequired(false) .setMinValue(0) - .setMaxValue(7) - ) + .setMaxValue(7), + ), ) .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild); @@ -100,7 +83,8 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise const hasPermission = await checkModeratorRole(interaction); if (!hasPermission) { await interaction.reply({ - content: "❌ You don't have permission to use moderation commands.\n" + + content: + "❌ You don't have permission to use moderation commands.\n" + "Ask an admin to set up moderator roles with `/config moderator-role`.", flags: MessageFlags.Ephemeral, }); @@ -128,7 +112,7 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise } catch (err) { logger.error( { err, command: "admin", userId: interaction.user.id }, - "[admin] command failed" + "[admin] command failed", ); try { @@ -147,7 +131,9 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise /** * Check if user has moderator role */ -async function checkModeratorRole(interaction: ChatInputCommandInteraction): Promise { +async function checkModeratorRole( + interaction: ChatInputCommandInteraction, +): Promise { if (!interaction.inGuild() || !interaction.member) { return false; } @@ -163,9 +149,7 @@ async function checkModeratorRole(interaction: ChatInputCommandInteraction): Pro // Check for moderator roles in database const roles = db - .prepare( - `SELECT role_id FROM moderator_roles WHERE guild_id = ?` - ) + .prepare(`SELECT role_id FROM moderator_roles WHERE guild_id = ?`) .all(guildId) as Array<{ role_id: string }>; if (roles.length === 0) { @@ -245,7 +229,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< duration, reason, }, - "[admin] User timed out" + "[admin] User timed out", ); } catch (err) { logger.error({ err, targetUser: targetUser.id }, "[admin] timeout failed"); @@ -323,7 +307,7 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise w[0].toUpperCase() + w.slice(1)) .join(" "); -} \ No newline at end of file +} From 9e90ad27a5edfc362ab70ab8a0a4e30559a58c59 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:33:27 -0500 Subject: [PATCH 3/7] Fixing this up --- src/commands/admin/admin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 9eafdd7d..aa36becc 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -6,7 +6,7 @@ import { MessageFlags, } from "discord.js"; import { logger } from "../../utils/logger.js"; -import { getDb } from "../../db/index.js"; +import { getDb } from "../../services/database/db.js"; /** * /admin From a6209ec507cf79752f9dc079457e32c0c76bf314 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:37:15 -0500 Subject: [PATCH 4/7] Adding in more --- src/commands/admin/admin.ts | 249 +++++++++++++++++++++++++++--------- 1 file changed, 187 insertions(+), 62 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index aa36becc..2a9635c8 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -1,5 +1,7 @@ +// src/commands/admin/admin.ts import { SlashCommandBuilder, + EmbedBuilder, PermissionFlagsBits, type ChatInputCommandInteraction, type GuildMember, @@ -7,15 +9,14 @@ import { } from "discord.js"; import { logger } from "../../utils/logger.js"; import { getDb } from "../../services/database/db.js"; +import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; +import { EmbedColors } from "../../utils/colors.js"; -/** - * /admin - * - * Role-based moderation commands: timeout, kick, ban - */ export const data = new SlashCommandBuilder() .setName("admin") - .setDescription("Moderation commands (role-based)") + .setDescription("Admin and moderation commands") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + // Moderation subcommands .addSubcommand((sub) => sub .setName("timeout") @@ -23,14 +24,13 @@ export const data = new SlashCommandBuilder() .addUserOption((opt) => opt.setName("user").setDescription("User to timeout").setRequired(true), ) - .addIntegerOption( - (opt) => - opt - .setName("duration") - .setDescription("Duration in minutes") - .setRequired(true) - .setMinValue(1) - .setMaxValue(40320), // 28 days max + .addIntegerOption((opt) => + opt + .setName("duration") + .setDescription("Duration in minutes") + .setRequired(true) + .setMinValue(1) + .setMaxValue(40320), ) .addStringOption((opt) => opt.setName("reason").setDescription("Reason for timeout").setRequired(false), @@ -66,11 +66,34 @@ export const data = new SlashCommandBuilder() .setMaxValue(7), ), ) - .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild); + // Bot stats subcommands + .addSubcommand((sub) => + sub + .setName("stats") + .setDescription("Show bot statistics (uptime, database, commands)"), + ) + .addSubcommand((sub) => + sub.setName("health").setDescription("Check bot and service health"), + ); export async function execute(interaction: ChatInputCommandInteraction): Promise { + const subcommand = interaction.options.getSubcommand(); + + // Handle stats and health (no moderation check needed) + if (subcommand === "stats") { + await interaction.deferReply(); + await handleStats(interaction); + return; + } + + if (subcommand === "health") { + await interaction.deferReply(); + await handleHealth(interaction); + return; + } + + // For moderation commands, check permissions try { - // Must be in a guild if (!interaction.inGuild()) { await interaction.reply({ content: "This command can only be used in a server.", @@ -79,7 +102,6 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise return; } - // Check if user has the required role const hasPermission = await checkModeratorRole(interaction); if (!hasPermission) { await interaction.reply({ @@ -91,8 +113,6 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise return; } - const subcommand = interaction.options.getSubcommand(); - switch (subcommand) { case "timeout": await handleTimeout(interaction); @@ -128,9 +148,6 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise } } -/** - * Check if user has moderator role - */ async function checkModeratorRole( interaction: ChatInputCommandInteraction, ): Promise { @@ -138,7 +155,6 @@ async function checkModeratorRole( return false; } - // Admins always have permission if (interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { return true; } @@ -147,20 +163,17 @@ async function checkModeratorRole( const db = getDb(); const guildId = interaction.guildId!; - // Check for moderator roles in database const roles = db .prepare(`SELECT role_id FROM moderator_roles WHERE guild_id = ?`) .all(guildId) as Array<{ role_id: string }>; if (roles.length === 0) { - // No moderator roles configured - only admins can use return false; } const member = interaction.member as GuildMember; const memberRoles = member.roles.cache; - // Check if user has any of the moderator roles return roles.some((r) => memberRoles.has(r.role_id)); } catch (err) { logger.error({ err }, "[admin] failed to check moderator role"); @@ -168,9 +181,6 @@ async function checkModeratorRole( } } -/** - * Handle timeout subcommand - */ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise { const targetUser = interaction.options.getUser("user", true); const duration = interaction.options.getInteger("duration", true); @@ -187,7 +197,6 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< try { const member = await interaction.guild.members.fetch(targetUser.id); - // Can't timeout bots or self if (member.user.bot) { await interaction.reply({ content: "❌ Cannot timeout bots.", @@ -204,7 +213,6 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< return; } - // Check role hierarchy const executor = interaction.member as GuildMember; if (member.roles.highest.position >= executor.roles.highest.position) { await interaction.reply({ @@ -214,7 +222,6 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< return; } - // Apply timeout const durationMs = duration * 60 * 1000; await member.timeout(durationMs, reason); @@ -223,12 +230,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< }); logger.info( - { - moderator: interaction.user.tag, - target: targetUser.tag, - duration, - reason, - }, + { moderator: interaction.user.tag, target: targetUser.tag, duration, reason }, "[admin] User timed out", ); } catch (err) { @@ -240,9 +242,6 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< } } -/** - * Handle kick subcommand - */ async function handleKick(interaction: ChatInputCommandInteraction): Promise { const targetUser = interaction.options.getUser("user", true); const reason = interaction.options.getString("reason") ?? "No reason provided"; @@ -258,7 +257,6 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { await interaction.reply({ @@ -285,7 +282,6 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise { const targetUser = interaction.options.getUser("user", true); const reason = interaction.options.getString("reason") ?? "No reason provided"; @@ -337,7 +325,6 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise null); - // Can't ban self if (targetUser.id === interaction.user.id) { await interaction.reply({ content: "❌ You cannot ban yourself.", @@ -346,7 +333,6 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise { + try { + const db = getDb(); + + const jokeCount = db.prepare("SELECT COUNT(*) as count FROM jokes").get() as { + count: number; + }; + + const coinFlipCount = db + .prepare("SELECT COUNT(*) as count FROM coin_flips") + .get() as { count: number }; + + const funUsage = await getFunUsageSnapshot(); + const totalCommands = Object.values(funUsage.totalsByCommand).reduce( + (sum, count) => sum + count, + 0 + ); + + const uptimeSeconds = process.uptime(); + const uptimeDays = Math.floor(uptimeSeconds / 86400); + const uptimeHours = Math.floor((uptimeSeconds % 86400) / 3600); + const uptimeMinutes = Math.floor((uptimeSeconds % 3600) / 60); + + const memUsage = process.memoryUsage(); + const memUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024); + const memTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024); + + const embed = new EmbedBuilder() + .setTitle("πŸ€– Bot Statistics") + .setColor(EmbedColors.Info) + .addFields( + { + name: "⏱️ Uptime", + value: `${uptimeDays}d ${uptimeHours}h ${uptimeMinutes}m`, + inline: true, + }, + { + name: "πŸ’Ύ Memory", + value: `${memUsedMB}MB / ${memTotalMB}MB`, + inline: true, + }, + { + name: "πŸ“Š Total Commands", + value: totalCommands.toString(), + inline: true, + }, + { + name: "🎭 Jokes", + value: jokeCount.count.toString(), + inline: true, + }, + { + name: "πŸͺ™ Coin Flips", + value: coinFlipCount.count.toString(), + inline: true, + }, + { + name: "πŸ‘₯ Unique Users", + value: Object.keys(funUsage.totalsByUser).length.toString(), + inline: true, + } + ) + .setFooter({ text: `Node ${process.version}` }) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + logger.info({ userId: interaction.user.id }, "[admin] Viewed bot statistics"); + } catch (error) { + logger.error({ error }, "[admin] stats failed"); + await interaction.editReply("❌ Failed to get statistics"); + } +} + +async function handleHealth(interaction: ChatInputCommandInteraction): Promise { + try { + const checks: { name: string; status: string; details?: string }[] = []; + + try { + const db = getDb(); + db.prepare("SELECT 1").get(); + checks.push({ name: "Database", status: "βœ… Healthy" }); + } catch (error) { + checks.push({ + name: "Database", + status: "❌ Error", + details: error instanceof Error ? error.message : "Unknown error", + }); + } + + const requiredEnvVars = [ + "DISCORD_TOKEN", + "DISCORD_APP_ID", + "GITHUB_TOKEN", + "GITHUB_OWNER", + "GITHUB_REPO", + ]; + + const missingVars = requiredEnvVars.filter((v) => !process.env[v]); + if (missingVars.length === 0) { + checks.push({ name: "Environment", status: "βœ… All vars set" }); + } else { + checks.push({ + name: "Environment", + status: "⚠️ Missing vars", + details: missingVars.join(", "), + }); + } + + const optionalKeys = [ + { name: "Anthropic API", key: "ANTHROPIC_API_KEY" }, + { name: "Weather API", key: "WEATHERAPI_KEY" }, + ]; + + optionalKeys.forEach(({ name, key }) => { + if (process.env[key]) { + checks.push({ name, status: "βœ… Configured" }); + } else { + checks.push({ name, status: "⚠️ Not configured" }); + } + }); + + const embed = new EmbedBuilder() + .setTitle("πŸ₯ Health Check") + .setColor(EmbedColors.Info) + .setDescription( + checks + .map((c) => + c.details + ? `**${c.name}:** ${c.status}\n ${c.details}` + : `**${c.name}:** ${c.status}` + ) + .join("\n\n") + ) + .setTimestamp(); + + await interaction.editReply({ embeds: [embed] }); + + logger.info({ userId: interaction.user.id }, "[admin] Viewed health check"); + } catch (error) { + logger.error({ error }, "[admin] health check failed"); + await interaction.editReply("❌ Failed to run health check"); + } +} \ No newline at end of file From 241fc718fd792e967f195b81a2bd6a88d7a5b345 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:37:20 -0500 Subject: [PATCH 5/7] style: auto-format with Prettier [skip-precheck] --- src/commands/admin/admin.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 2a9635c8..5308b464 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -397,7 +397,7 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise sum + count, - 0 + 0, ); const uptimeSeconds = process.uptime(); @@ -442,7 +442,7 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise c.details ? `**${c.name}:** ${c.status}\n ${c.details}` - : `**${c.name}:** ${c.status}` + : `**${c.name}:** ${c.status}`, ) - .join("\n\n") + .join("\n\n"), ) .setTimestamp(); @@ -525,4 +525,4 @@ async function handleHealth(interaction: ChatInputCommandInteraction): Promise Date: Sun, 18 Jan 2026 15:50:52 -0500 Subject: [PATCH 6/7] Fixing this up --- src/commands/admin/admin.ts | 429 +++++++++++++++--------------------- 1 file changed, 180 insertions(+), 249 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index 5308b464..e29df199 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -1,4 +1,5 @@ // src/commands/admin/admin.ts + import { SlashCommandBuilder, EmbedBuilder, @@ -12,6 +13,14 @@ import { getDb } from "../../services/database/db.js"; import { getFunUsageSnapshot } from "../../services/fun/funUsageStore.js"; import { EmbedColors } from "../../utils/colors.js"; +/** + * NOTE + * This file intentionally centralizes reply behavior: + * - We deferReply once per execution path + * - We prefer editReply after deferring + * - We fall back to reply if not deferred/replied + */ + export const data = new SlashCommandBuilder() .setName("admin") .setDescription("Admin and moderation commands") @@ -68,116 +77,133 @@ export const data = new SlashCommandBuilder() ) // Bot stats subcommands .addSubcommand((sub) => - sub - .setName("stats") - .setDescription("Show bot statistics (uptime, database, commands)"), + sub.setName("stats").setDescription("Show bot statistics (uptime, database, commands)"), ) - .addSubcommand((sub) => - sub.setName("health").setDescription("Check bot and service health"), - ); + .addSubcommand((sub) => sub.setName("health").setDescription("Check bot and service health")) + .setDMPermission(true); + +async function safeEphemeralMessage( + interaction: ChatInputCommandInteraction, + content: string, +): Promise { + try { + if (interaction.deferred || interaction.replied) { + await interaction.editReply({ content }); + return; + } + await interaction.reply({ content, flags: MessageFlags.Ephemeral }); + } catch (err) { + logger.error({ err }, "[admin] failed to send safe reply"); + } +} + +async function ensureDeferred( + interaction: ChatInputCommandInteraction, + ephemeral: boolean, +): Promise { + if (interaction.deferred || interaction.replied) return; + + // discord.js supports either { ephemeral: true } or flags + // Using flags keeps compatibility with the rest of this repo style. + await interaction.deferReply({ + flags: ephemeral ? MessageFlags.Ephemeral : undefined, + }); +} + +function isModeratorOrManager(interaction: ChatInputCommandInteraction): boolean { + return Boolean(interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)); +} + +async function checkModeratorRole(interaction: ChatInputCommandInteraction): Promise { + // Hard guard + if (!interaction.inGuild() || !interaction.guildId || !interaction.member) return false; + + // Manage Guild always allowed + if (isModeratorOrManager(interaction)) return true; + + try { + const db = getDb(); + const roles = db + .prepare(`SELECT role_id FROM moderator_roles WHERE guild_id = ?`) + .all(interaction.guildId) as Array<{ role_id: string }>; + + if (!roles.length) return false; + + const member = interaction.member as GuildMember; + const memberRoles = member.roles.cache; + return roles.some((r) => memberRoles.has(r.role_id)); + } catch (err) { + logger.error({ err, guildId: interaction.guildId }, "[admin] failed to check moderator role"); + return false; + } +} export async function execute(interaction: ChatInputCommandInteraction): Promise { const subcommand = interaction.options.getSubcommand(); - // Handle stats and health (no moderation check needed) - if (subcommand === "stats") { - await interaction.deferReply(); - await handleStats(interaction); - return; + // Route 1: stats and health can run anywhere + if (subcommand === "stats" || subcommand === "health") { + await ensureDeferred(interaction, false); + + try { + if (subcommand === "stats") { + await handleStats(interaction); + } else { + await handleHealth(interaction); + } + return; + } catch (err) { + logger.error( + { err, command: "admin", subcommand, userId: interaction.user.id }, + "[admin] stats/health failed", + ); + await safeEphemeralMessage(interaction, "Something went wrong. Please try again later."); + return; + } } - if (subcommand === "health") { - await interaction.deferReply(); - await handleHealth(interaction); + // Route 2: moderation commands must be in guild + if (!interaction.inGuild() || !interaction.guildId) { + await safeEphemeralMessage(interaction, "This command can only be used in a server."); return; } - // For moderation commands, check permissions - try { - if (!interaction.inGuild()) { - await interaction.reply({ - content: "This command can only be used in a server.", - flags: MessageFlags.Ephemeral, - }); - return; - } + // Moderation commands should be ephemeral to reduce channel noise + await ensureDeferred(interaction, true); + try { const hasPermission = await checkModeratorRole(interaction); if (!hasPermission) { - await interaction.reply({ - content: - "❌ You don't have permission to use moderation commands.\n" + + await safeEphemeralMessage( + interaction, + [ + "You do not have permission to use moderation commands.", "Ask an admin to set up moderator roles with `/config moderator-role`.", - flags: MessageFlags.Ephemeral, - }); + ].join("\n"), + ); return; } switch (subcommand) { case "timeout": await handleTimeout(interaction); - break; + return; case "kick": await handleKick(interaction); - break; + return; case "ban": await handleBan(interaction); - break; + return; default: - await interaction.reply({ - content: "Unknown subcommand.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "Unknown subcommand."); + return; } } catch (err) { logger.error( - { err, command: "admin", userId: interaction.user.id }, - "[admin] command failed", + { err, command: "admin", subcommand, userId: interaction.user.id, guildId: interaction.guildId }, + "[admin] moderation command failed", ); - - try { - if (!interaction.replied && !interaction.deferred) { - await interaction.reply({ - content: "Something went wrong. Please try again later.", - flags: MessageFlags.Ephemeral, - }); - } - } catch (replyErr) { - logger.error({ err: replyErr }, "[admin] failed to send error reply"); - } - } -} - -async function checkModeratorRole( - interaction: ChatInputCommandInteraction, -): Promise { - if (!interaction.inGuild() || !interaction.member) { - return false; - } - - if (interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - return true; - } - - try { - const db = getDb(); - const guildId = interaction.guildId!; - - const roles = db - .prepare(`SELECT role_id FROM moderator_roles WHERE guild_id = ?`) - .all(guildId) as Array<{ role_id: string }>; - - if (roles.length === 0) { - return false; - } - - const member = interaction.member as GuildMember; - const memberRoles = member.roles.cache; - - return roles.some((r) => memberRoles.has(r.role_id)); - } catch (err) { - logger.error({ err }, "[admin] failed to check moderator role"); - return false; + await safeEphemeralMessage(interaction, "Something went wrong. Please try again later."); } } @@ -187,10 +213,7 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< const reason = interaction.options.getString("reason") ?? "No reason provided"; if (!interaction.guild) { - await interaction.reply({ - content: "This command can only be used in a server.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "This command can only be used in a server."); return; } @@ -198,47 +221,38 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< const member = await interaction.guild.members.fetch(targetUser.id); if (member.user.bot) { - await interaction.reply({ - content: "❌ Cannot timeout bots.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "Cannot timeout bots."); return; } if (member.id === interaction.user.id) { - await interaction.reply({ - content: "❌ You cannot timeout yourself.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "You cannot timeout yourself."); return; } const executor = interaction.member as GuildMember; if (member.roles.highest.position >= executor.roles.highest.position) { - await interaction.reply({ - content: "❌ You cannot timeout someone with an equal or higher role.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "You cannot timeout someone with an equal or higher role."); return; } const durationMs = duration * 60 * 1000; await member.timeout(durationMs, reason); - await interaction.reply({ - content: `βœ… ${targetUser.tag} has been timed out for ${duration} minute(s).\n**Reason:** ${reason}`, - }); + await interaction.editReply( + `Timed out **${targetUser.tag}** for **${duration}** minute(s).\nReason: ${reason}`, + ); logger.info( - { moderator: interaction.user.tag, target: targetUser.tag, duration, reason }, - "[admin] User timed out", + { moderator: interaction.user.tag, target: targetUser.tag, duration, reason, guildId: interaction.guildId }, + "[admin] user timed out", ); } catch (err) { - logger.error({ err, targetUser: targetUser.id }, "[admin] timeout failed"); - await interaction.reply({ - content: "❌ Failed to timeout user. Check my permissions and role position.", - flags: MessageFlags.Ephemeral, - }); + logger.error( + { err, targetUser: targetUser.id, guildId: interaction.guildId }, + "[admin] timeout failed", + ); + await safeEphemeralMessage(interaction, "Failed to timeout user. Check my permissions and role position."); } } @@ -247,10 +261,7 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { - await interaction.reply({ - content: "❌ You cannot kick someone with an equal or higher role.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "You cannot kick someone with an equal or higher role."); return; } if (!member.kickable) { - await interaction.reply({ - content: "❌ I don't have permission to kick this user.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "I do not have permission to kick this user."); return; } await member.kick(reason); - await interaction.reply({ - content: `βœ… ${targetUser.tag} has been kicked.\n**Reason:** ${reason}`, - }); + await interaction.editReply(`Kicked **${targetUser.tag}**.\nReason: ${reason}`); logger.info( - { moderator: interaction.user.tag, target: targetUser.tag, reason }, - "[admin] User kicked", + { moderator: interaction.user.tag, target: targetUser.tag, reason, guildId: interaction.guildId }, + "[admin] user kicked", ); } catch (err) { - logger.error({ err, targetUser: targetUser.id }, "[admin] kick failed"); - await interaction.reply({ - content: "❌ Failed to kick user. Check my permissions and role position.", - flags: MessageFlags.Ephemeral, - }); + logger.error({ err, targetUser: targetUser.id, guildId: interaction.guildId }, "[admin] kick failed"); + await safeEphemeralMessage(interaction, "Failed to kick user. Check my permissions and role position."); } } @@ -315,47 +309,32 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise null); - if (targetUser.id === interaction.user.id) { - await interaction.reply({ - content: "❌ You cannot ban yourself.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "You cannot ban yourself."); return; } + const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); + if (member) { if (member.user.bot) { - await interaction.reply({ - content: "❌ Cannot ban bots.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "Cannot ban bots."); return; } const executor = interaction.member as GuildMember; if (member.roles.highest.position >= executor.roles.highest.position) { - await interaction.reply({ - content: "❌ You cannot ban someone with an equal or higher role.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "You cannot ban someone with an equal or higher role."); return; } if (!member.bannable) { - await interaction.reply({ - content: "❌ I don't have permission to ban this user.", - flags: MessageFlags.Ephemeral, - }); + await safeEphemeralMessage(interaction, "I do not have permission to ban this user."); return; } } @@ -365,20 +344,16 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise 0 ? `\n**Messages deleted:** Last ${deleteDays} day(s)` : ""}`, - }); + const deletedText = deleteDays > 0 ? `\nMessages deleted: last ${deleteDays} day(s)` : ""; + await interaction.editReply(`Banned **${targetUser.tag}**.\nReason: ${reason}${deletedText}`); logger.info( - { moderator: interaction.user.tag, target: targetUser.tag, reason, deleteDays }, - "[admin] User banned", + { moderator: interaction.user.tag, target: targetUser.tag, reason, deleteDays, guildId: interaction.guildId }, + "[admin] user banned", ); } catch (err) { - logger.error({ err, targetUser: targetUser.id }, "[admin] ban failed"); - await interaction.reply({ - content: "❌ Failed to ban user. Check my permissions and role position.", - flags: MessageFlags.Ephemeral, - }); + logger.error({ err, targetUser: targetUser.id, guildId: interaction.guildId }, "[admin] ban failed"); + await safeEphemeralMessage(interaction, "Failed to ban user. Check my permissions and role position."); } } @@ -386,19 +361,13 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise sum + count, - 0, - ); + + const totalsByCommand = (funUsage?.totalsByCommand ?? {}) as Record; + const totalCommands = Object.values(totalsByCommand).reduce((sum, count) => sum + (count ?? 0), 0); const uptimeSeconds = process.uptime(); const uptimeDays = Math.floor(uptimeSeconds / 86400); @@ -410,49 +379,25 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise !process.env[v]?.trim()); - const missingVars = requiredEnvVars.filter((v) => !process.env[v]); - if (missingVars.length === 0) { - checks.push({ name: "Environment", status: "βœ… All vars set" }); + if (missingRequired.length === 0) { + checks.push({ name: "Required env", status: "All set" }); } else { - checks.push({ - name: "Environment", - status: "⚠️ Missing vars", - details: missingVars.join(", "), - }); + checks.push({ name: "Required env", status: "Missing", details: missingRequired.join(", ") }); } - const optionalKeys = [ - { name: "Anthropic API", key: "ANTHROPIC_API_KEY" }, - { name: "Weather API", key: "WEATHERAPI_KEY" }, + const optionalEnv = [ + { name: "OpenAI", key: "OPENAI_API_KEY" }, + { name: "GitHub", key: "GITHUB_TOKEN" }, + { name: "WeatherAPI", key: "WEATHERAPI_KEY" }, ]; - optionalKeys.forEach(({ name, key }) => { - if (process.env[key]) { - checks.push({ name, status: "βœ… Configured" }); - } else { - checks.push({ name, status: "⚠️ Not configured" }); - } - }); + for (const item of optionalEnv) { + checks.push({ + name: item.name, + status: process.env[item.key]?.trim() ? "Configured" : "Not configured", + }); + } const embed = new EmbedBuilder() - .setTitle("πŸ₯ Health Check") + .setTitle("Health Check") .setColor(EmbedColors.Info) .setDescription( checks - .map((c) => - c.details - ? `**${c.name}:** ${c.status}\n ${c.details}` - : `**${c.name}:** ${c.status}`, - ) + .map((c) => (c.details ? `**${c.name}:** ${c.status}\n${c.details}` : `**${c.name}:** ${c.status}`)) .join("\n\n"), ) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); - logger.info({ userId: interaction.user.id }, "[admin] Viewed health check"); + logger.info({ userId: interaction.user.id }, "[admin] viewed health check"); } catch (error) { logger.error({ error }, "[admin] health check failed"); - await interaction.editReply("❌ Failed to run health check"); + await safeEphemeralMessage(interaction, "Failed to run health check."); } -} +} \ No newline at end of file From 70a74f846a0a1d549858214dbc7a05ad43c2811c Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Sun, 18 Jan 2026 15:50:59 -0500 Subject: [PATCH 7/7] style: auto-format with Prettier [skip-precheck] --- src/commands/admin/admin.ts | 154 +++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 30 deletions(-) diff --git a/src/commands/admin/admin.ts b/src/commands/admin/admin.ts index e29df199..e1829758 100644 --- a/src/commands/admin/admin.ts +++ b/src/commands/admin/admin.ts @@ -77,9 +77,13 @@ export const data = new SlashCommandBuilder() ) // Bot stats subcommands .addSubcommand((sub) => - sub.setName("stats").setDescription("Show bot statistics (uptime, database, commands)"), + sub + .setName("stats") + .setDescription("Show bot statistics (uptime, database, commands)"), + ) + .addSubcommand((sub) => + sub.setName("health").setDescription("Check bot and service health"), ) - .addSubcommand((sub) => sub.setName("health").setDescription("Check bot and service health")) .setDMPermission(true); async function safeEphemeralMessage( @@ -114,7 +118,9 @@ function isModeratorOrManager(interaction: ChatInputCommandInteraction): boolean return Boolean(interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)); } -async function checkModeratorRole(interaction: ChatInputCommandInteraction): Promise { +async function checkModeratorRole( + interaction: ChatInputCommandInteraction, +): Promise { // Hard guard if (!interaction.inGuild() || !interaction.guildId || !interaction.member) return false; @@ -133,7 +139,10 @@ async function checkModeratorRole(interaction: ChatInputCommandInteraction): Pro const memberRoles = member.roles.cache; return roles.some((r) => memberRoles.has(r.role_id)); } catch (err) { - logger.error({ err, guildId: interaction.guildId }, "[admin] failed to check moderator role"); + logger.error( + { err, guildId: interaction.guildId }, + "[admin] failed to check moderator role", + ); return false; } } @@ -157,7 +166,10 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise { err, command: "admin", subcommand, userId: interaction.user.id }, "[admin] stats/health failed", ); - await safeEphemeralMessage(interaction, "Something went wrong. Please try again later."); + await safeEphemeralMessage( + interaction, + "Something went wrong. Please try again later.", + ); return; } } @@ -200,10 +212,19 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise } } catch (err) { logger.error( - { err, command: "admin", subcommand, userId: interaction.user.id, guildId: interaction.guildId }, + { + err, + command: "admin", + subcommand, + userId: interaction.user.id, + guildId: interaction.guildId, + }, "[admin] moderation command failed", ); - await safeEphemeralMessage(interaction, "Something went wrong. Please try again later."); + await safeEphemeralMessage( + interaction, + "Something went wrong. Please try again later.", + ); } } @@ -232,7 +253,10 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< const executor = interaction.member as GuildMember; if (member.roles.highest.position >= executor.roles.highest.position) { - await safeEphemeralMessage(interaction, "You cannot timeout someone with an equal or higher role."); + await safeEphemeralMessage( + interaction, + "You cannot timeout someone with an equal or higher role.", + ); return; } @@ -244,7 +268,13 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< ); logger.info( - { moderator: interaction.user.tag, target: targetUser.tag, duration, reason, guildId: interaction.guildId }, + { + moderator: interaction.user.tag, + target: targetUser.tag, + duration, + reason, + guildId: interaction.guildId, + }, "[admin] user timed out", ); } catch (err) { @@ -252,7 +282,10 @@ async function handleTimeout(interaction: ChatInputCommandInteraction): Promise< { err, targetUser: targetUser.id, guildId: interaction.guildId }, "[admin] timeout failed", ); - await safeEphemeralMessage(interaction, "Failed to timeout user. Check my permissions and role position."); + await safeEphemeralMessage( + interaction, + "Failed to timeout user. Check my permissions and role position.", + ); } } @@ -280,12 +313,18 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { - await safeEphemeralMessage(interaction, "You cannot kick someone with an equal or higher role."); + await safeEphemeralMessage( + interaction, + "You cannot kick someone with an equal or higher role.", + ); return; } if (!member.kickable) { - await safeEphemeralMessage(interaction, "I do not have permission to kick this user."); + await safeEphemeralMessage( + interaction, + "I do not have permission to kick this user.", + ); return; } @@ -294,12 +333,23 @@ async function handleKick(interaction: ChatInputCommandInteraction): Promise= executor.roles.highest.position) { - await safeEphemeralMessage(interaction, "You cannot ban someone with an equal or higher role."); + await safeEphemeralMessage( + interaction, + "You cannot ban someone with an equal or higher role.", + ); return; } if (!member.bannable) { - await safeEphemeralMessage(interaction, "I do not have permission to ban this user."); + await safeEphemeralMessage( + interaction, + "I do not have permission to ban this user.", + ); return; } } @@ -344,16 +400,31 @@ async function handleBan(interaction: ChatInputCommandInteraction): Promise 0 ? `\nMessages deleted: last ${deleteDays} day(s)` : ""; - await interaction.editReply(`Banned **${targetUser.tag}**.\nReason: ${reason}${deletedText}`); + const deletedText = + deleteDays > 0 ? `\nMessages deleted: last ${deleteDays} day(s)` : ""; + await interaction.editReply( + `Banned **${targetUser.tag}**.\nReason: ${reason}${deletedText}`, + ); logger.info( - { moderator: interaction.user.tag, target: targetUser.tag, reason, deleteDays, guildId: interaction.guildId }, + { + moderator: interaction.user.tag, + target: targetUser.tag, + reason, + deleteDays, + guildId: interaction.guildId, + }, "[admin] user banned", ); } catch (err) { - logger.error({ err, targetUser: targetUser.id, guildId: interaction.guildId }, "[admin] ban failed"); - await safeEphemeralMessage(interaction, "Failed to ban user. Check my permissions and role position."); + logger.error( + { err, targetUser: targetUser.id, guildId: interaction.guildId }, + "[admin] ban failed", + ); + await safeEphemeralMessage( + interaction, + "Failed to ban user. Check my permissions and role position.", + ); } } @@ -361,13 +432,20 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise; - const totalCommands = Object.values(totalsByCommand).reduce((sum, count) => sum + (count ?? 0), 0); + const totalCommands = Object.values(totalsByCommand).reduce( + (sum, count) => sum + (count ?? 0), + 0, + ); const uptimeSeconds = process.uptime(); const uptimeDays = Math.floor(uptimeSeconds / 86400); @@ -382,12 +460,20 @@ async function handleStats(interaction: ChatInputCommandInteraction): Promise (c.details ? `**${c.name}:** ${c.status}\n${c.details}` : `**${c.name}:** ${c.status}`)) + .map((c) => + c.details + ? `**${c.name}:** ${c.status}\n${c.details}` + : `**${c.name}:** ${c.status}`, + ) .join("\n\n"), ) .setTimestamp(); @@ -456,4 +550,4 @@ async function handleHealth(interaction: ChatInputCommandInteraction): Promise