From 7c2a99daf5b1b77eb3ce815de411ceaf9f0349f1 Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Thu, 17 Apr 2025 01:05:10 -0400 Subject: [PATCH] chore: improve safety of commands --- config.example.json | 1 + src/commands/fun/achievement.ts | 3 +- src/commands/fun/counting.ts | 14 ++-- src/commands/fun/fact.ts | 5 +- src/commands/fun/giveaway.ts | 27 ++++++-- src/commands/fun/leaderboard.ts | 5 +- src/commands/fun/rank.ts | 14 ++-- src/commands/moderation/ban.ts | 90 ++++++++++++++++---------- src/commands/moderation/kick.ts | 67 +++++++++++-------- src/commands/moderation/mute.ts | 71 +++++++++++--------- src/commands/moderation/unban.ts | 41 ++++++------ src/commands/moderation/unmute.ts | 49 +++++++------- src/commands/moderation/warn.ts | 54 +++++++++------- src/commands/testing/testJoin.ts | 14 ++-- src/commands/testing/testLeave.ts | 13 ++-- src/commands/util/config.ts | 10 +-- src/commands/util/help.ts | 10 +-- src/commands/util/members.ts | 6 +- src/commands/util/ping.ts | 2 +- src/commands/util/recalculatelevels.ts | 11 ++-- src/commands/util/reconnect.ts | 9 ++- src/commands/util/restart.ts | 10 +-- src/commands/util/server.ts | 4 +- src/commands/util/user-info.ts | 10 ++- src/commands/util/xp.ts | 16 ++--- src/discord-bot.ts | 3 +- src/structures/ExtendedClient.ts | 4 ++ src/types/ConfigTypes.ts | 1 + 28 files changed, 329 insertions(+), 235 deletions(-) diff --git a/config.example.json b/config.example.json index 25d2f48..19f9201 100644 --- a/config.example.json +++ b/config.example.json @@ -2,6 +2,7 @@ "token": "DISCORD_BOT_TOKEN", "clientId": "DISCORD_BOT_ID", "guildId": "DISCORD_SERVER_ID", + "serverInvite": "DISCORD_SERVER_INVITE_LINK", "database": { "dbConnectionString": "POSTGRESQL_CONNECTION_STRING", "maxRetryAttempts": "MAX_RETRY_ATTEMPTS", diff --git a/src/commands/fun/achievement.ts b/src/commands/fun/achievement.ts index e328956..c1ef61b 100644 --- a/src/commands/fun/achievement.ts +++ b/src/commands/fun/achievement.ts @@ -148,8 +148,9 @@ const command = { ), async execute(interaction: ChatInputCommandInteraction) { - await interaction.deferReply(); + if (!interaction.isChatInputCommand() || !interaction.guild) return; + await interaction.deferReply(); const subcommand = interaction.options.getSubcommand(); switch (subcommand) { diff --git a/src/commands/fun/counting.ts b/src/commands/fun/counting.ts index ce7cb6c..773ee1d 100644 --- a/src/commands/fun/counting.ts +++ b/src/commands/fun/counting.ts @@ -33,8 +33,9 @@ const command: SubcommandCommand = { ), execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand() || !interaction.guild) return; + await interaction.deferReply(); const subcommand = interaction.options.getSubcommand(); if (subcommand === 'status') { @@ -82,33 +83,30 @@ const command: SubcommandCommand = { }); } - await interaction.reply({ embeds: [embed] }); + await interaction.editReply({ embeds: [embed] }); } else if (subcommand === 'setcount') { if ( !interaction.memberPermissions?.has( PermissionsBitField.Flags.Administrator, ) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You need administrator permissions to use this command.', - flags: ['Ephemeral'], }); return; } const count = interaction.options.getInteger('count'); if (count === null) { - await interaction.reply({ + await interaction.editReply({ content: 'Invalid count specified.', - flags: ['Ephemeral'], }); return; } await setCount(count); - await interaction.reply({ + await interaction.editReply({ content: `Count has been set to **${count}**. The next number should be **${count + 1}**.`, - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/fun/fact.ts b/src/commands/fun/fact.ts index 45869e0..109bdd3 100644 --- a/src/commands/fun/fact.ts +++ b/src/commands/fun/fact.ts @@ -74,12 +74,11 @@ const command: SubcommandCommand = { ), execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand() || !interaction.guild) return; await interaction.deferReply({ flags: ['Ephemeral'], }); - await interaction.editReply('Processing...'); const config = loadConfig(); const subcommand = interaction.options.getSubcommand(); @@ -100,7 +99,7 @@ const command: SubcommandCommand = { }); if (!isAdmin) { - const approvalChannel = interaction.guild?.channels.cache.get( + const approvalChannel = interaction.guild.channels.cache.get( config.channels.factApproval, ); diff --git a/src/commands/fun/giveaway.ts b/src/commands/fun/giveaway.ts index 29a557c..9dcdbfc 100644 --- a/src/commands/fun/giveaway.ts +++ b/src/commands/fun/giveaway.ts @@ -18,6 +18,7 @@ import { builder, } from '@/util/giveaways/giveawayManager.js'; import { createPaginationButtons } from '@/util/helpers.js'; +import { loadConfig } from '@/util/configLoader'; const command: SubcommandCommand = { data: new SlashCommandBuilder() @@ -53,16 +54,30 @@ const command: SubcommandCommand = { ), execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + const config = loadConfig(); + const communityManagerRoleId = config.roles.staffRoles.find( + (role) => role.name === 'Community Manager', + )?.roleId; + + if (!communityManagerRoleId) { + await interaction.reply({ + content: + 'Community Manager role not found in the configuration. Please contact a server admin.', + flags: ['Ephemeral'], + }); + return; + } if ( - !interaction.memberPermissions?.has( - PermissionsBitField.Flags.ModerateMembers, - ) + !interaction.guild.members.cache + .find((member) => member.id === interaction.user.id) + ?.roles.cache.has(communityManagerRoleId) ) { await interaction.reply({ content: 'You do not have permission to manage giveaways.', - ephemeral: true, + flags: ['Ephemeral'], }); return; } @@ -152,7 +167,7 @@ async function handleListGiveaways(interaction: ChatInputCommandInteraction) { if (i.user.id !== interaction.user.id) { await i.reply({ content: 'You cannot use these buttons.', - ephemeral: true, + flags: ['Ephemeral'], }); return; } diff --git a/src/commands/fun/leaderboard.ts b/src/commands/fun/leaderboard.ts index 5d33eb4..e2d39f3 100644 --- a/src/commands/fun/leaderboard.ts +++ b/src/commands/fun/leaderboard.ts @@ -22,13 +22,12 @@ const command: OptionsCommand = { .setRequired(false), ), execute: async (interaction) => { - if (!interaction.guild) return; + if (!interaction.isChatInputCommand() || !interaction.guild) return; await interaction.deferReply(); try { - const usersPerPage = - (interaction.options.get('limit')?.value as number) || 10; + const usersPerPage = interaction.options.getInteger('limit') || 10; const allUsers = await getLevelLeaderboard(100); diff --git a/src/commands/fun/rank.ts b/src/commands/fun/rank.ts index 7863779..a9908ce 100644 --- a/src/commands/fun/rank.ts +++ b/src/commands/fun/rank.ts @@ -15,18 +15,16 @@ const command: OptionsCommand = { .setRequired(false), ), execute: async (interaction) => { - const member = await interaction.guild?.members.fetch( - (interaction.options.get('user')?.value as string) || interaction.user.id, - ); - - if (!member) { - await interaction.reply('User not found in this server.'); - return; - } + if (!interaction.isChatInputCommand() || !interaction.guild) return; await interaction.deferReply(); try { + const member = await interaction.guild.members.fetch( + (interaction.options.get('user')?.value as string) || + interaction.user.id, + ); + const userData = await getUserLevel(member.id); const rankCard = await generateRankCard(member, userData); diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index 84a5ee8..e1049a5 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -3,6 +3,7 @@ import { PermissionsBitField, SlashCommandBuilder } from 'discord.js'; import { updateMember, updateMemberModerationHistory } from '@/db/db.js'; import { parseDuration, scheduleUnban } from '@/util/helpers.js'; import { OptionsCommand } from '@/types/CommandTypes.js'; +import { loadConfig } from '@/util/configLoader.js'; import logAction from '@/util/logging/logAction.js'; const command: OptionsCommand = { @@ -30,40 +31,62 @@ const command: OptionsCommand = { .setRequired(false), ), execute: async (interaction) => { - const moderator = await interaction.guild?.members.fetch( - interaction.user.id, - ); - const member = await interaction.guild?.members.fetch( - interaction.options.get('member')!.value as string, - ); - const reason = interaction.options.get('reason')?.value as string; - const banDuration = interaction.options.get('duration')?.value as - | string - | undefined; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - if ( - !interaction.memberPermissions?.has( - PermissionsBitField.Flags.BanMembers, - ) || - moderator!.roles.highest.position <= member!.roles.highest.position || - !member?.bannable - ) { - await interaction.reply({ - content: - 'You do not have permission to ban members or this member cannot be banned.', - flags: ['Ephemeral'], - }); - return; - } + await interaction.deferReply({ flags: ['Ephemeral'] }); try { - await member.user.send( - banDuration - ? `You have been banned from ${interaction.guild!.name} for ${banDuration}. Reason: ${reason}. You can join back at ${new Date( - Date.now() + parseDuration(banDuration), - ).toUTCString()} using the link below:\nhttps://discord.gg/KRTGjxx7gY` - : `You been indefinitely banned from ${interaction.guild!.name}. Reason: ${reason}.`, + const moderator = await interaction.guild.members.fetch( + interaction.user.id, ); + const member = await interaction.guild.members.fetch( + interaction.options.get('member')!.value as string, + ); + const reason = interaction.options.get('reason')?.value as string; + const banDuration = interaction.options.get('duration')?.value as + | string + | undefined; + + if ( + !interaction.memberPermissions?.has( + PermissionsBitField.Flags.BanMembers, + ) + ) { + await interaction.reply({ + content: 'You do not have permission to ban members.', + flags: ['Ephemeral'], + }); + return; + } + + if (moderator.roles.highest.position <= member.roles.highest.position) { + await interaction.reply({ + content: + 'You cannot ban a member with equal or higher role than yours.', + flags: ['Ephemeral'], + }); + return; + } + + if (!member.bannable) { + await interaction.reply({ + content: 'I do not have permission to ban this member.', + flags: ['Ephemeral'], + }); + return; + } + + try { + await member.user.send( + banDuration + ? `You have been banned from ${interaction.guild!.name} for ${banDuration}. Reason: ${reason}. You can join back at ${new Date( + Date.now() + parseDuration(banDuration), + ).toUTCString()} using the link below:\n${interaction.guild.vanityURLCode ?? loadConfig().serverInvite}` + : `You been indefinitely banned from ${interaction.guild!.name}. Reason: ${reason}.`, + ); + } catch (error) { + console.error('Failed to send DM:', error); + } await member.ban({ reason }); if (banDuration) { @@ -97,20 +120,19 @@ const command: OptionsCommand = { guild: interaction.guild!, action: 'ban', target: member, - moderator: moderator!, + moderator, reason, }); - await interaction.reply({ + await interaction.editReply({ content: banDuration ? `<@${member.id}> has been banned for ${banDuration}. Reason: ${reason}` : `<@${member.id}> has been indefinitely banned. Reason: ${reason}`, }); } catch (error) { console.error('Ban command error:', error); - await interaction.reply({ + await interaction.editReply({ content: 'Unable to ban member.', - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 5f49501..fce2ab7 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -2,6 +2,7 @@ import { PermissionsBitField, SlashCommandBuilder } from 'discord.js'; import { updateMemberModerationHistory } from '@/db/db.js'; import { OptionsCommand } from '@/types/CommandTypes.js'; +import { loadConfig } from '@/util/configLoader.js'; import logAction from '@/util/logging/logAction.js'; const command: OptionsCommand = { @@ -21,33 +22,48 @@ const command: OptionsCommand = { .setRequired(true), ), execute: async (interaction) => { - const moderator = await interaction.guild?.members.fetch( - interaction.user.id, - ); - const member = await interaction.guild?.members.fetch( - interaction.options.get('member')!.value as string, - ); - const reason = interaction.options.get('reason')?.value as string; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - if ( - !interaction.memberPermissions?.has( - PermissionsBitField.Flags.KickMembers, - ) || - moderator!.roles.highest.position <= member!.roles.highest.position || - !member?.kickable - ) { - await interaction.reply({ - content: - 'You do not have permission to kick members or this member cannot be kicked.', - flags: ['Ephemeral'], - }); - return; - } + await interaction.deferReply({ flags: ['Ephemeral'] }); try { + const moderator = await interaction.guild.members.fetch( + interaction.user.id, + ); + const member = await interaction.guild.members.fetch( + interaction.options.get('member')!.value as string, + ); + const reason = interaction.options.get('reason')?.value as string; + + if ( + !interaction.memberPermissions?.has( + PermissionsBitField.Flags.KickMembers, + ) + ) { + await interaction.editReply({ + content: 'You do not have permission to kick members.', + }); + return; + } + + if (moderator!.roles.highest.position <= member.roles.highest.position) { + await interaction.editReply({ + content: + 'You cannot kick a member with equal or higher role than yours.', + }); + return; + } + + if (!member.kickable) { + await interaction.editReply({ + content: 'I do not have permission to kick this member.', + }); + return; + } + try { await member.user.send( - `You have been kicked from ${interaction.guild!.name}. Reason: ${reason}. You can join back at: \nhttps://discord.gg/KRTGjxx7gY`, + `You have been kicked from ${interaction.guild!.name}. Reason: ${reason}. You can join back at: \n${interaction.guild.vanityURLCode ?? loadConfig().serverInvite}`, ); } catch (error) { console.error('Failed to send DM to kicked user:', error); @@ -68,18 +84,17 @@ const command: OptionsCommand = { guild: interaction.guild!, action: 'kick', target: member, - moderator: moderator!, + moderator, reason, }); - await interaction.reply({ + await interaction.editReply({ content: `<@${member.id}> has been kicked. Reason: ${reason}`, }); } catch (error) { console.error('Kick command error:', error); - await interaction.reply({ + await interaction.editReply({ content: 'Unable to kick member.', - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index d2f8cc4..ef7a506 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -30,38 +30,52 @@ const command: OptionsCommand = { .setRequired(true), ), execute: async (interaction) => { - const moderator = await interaction.guild?.members.fetch( - interaction.user.id, - ); - const member = await interaction.guild?.members.fetch( - interaction.options.get('member')!.value as string, - ); - const reason = interaction.options.get('reason')?.value as string; - const muteDuration = interaction.options.get('duration')?.value as string; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - if ( - !interaction.memberPermissions?.has( - PermissionsBitField.Flags.ModerateMembers, - ) || - moderator!.roles.highest.position <= member!.roles.highest.position || - !member?.moderatable - ) { - await interaction.reply({ - content: - 'You do not have permission to timeout members or this member cannot be timed out.', - flags: ['Ephemeral'], - }); - return; - } + await interaction.deferReply({ flags: ['Ephemeral'] }); try { + const moderator = await interaction.guild.members.fetch( + interaction.user.id, + ); + const member = await interaction.guild.members.fetch( + interaction.options.get('member')!.value as string, + ); + const reason = interaction.options.get('reason')?.value as string; + const muteDuration = interaction.options.get('duration')?.value as string; + + if ( + !interaction.memberPermissions?.has( + PermissionsBitField.Flags.KickMembers, + ) + ) { + await interaction.editReply({ + content: 'You do not have permission to mute members.', + }); + return; + } + + if (moderator.roles.highest.position <= member.roles.highest.position) { + await interaction.editReply({ + content: + 'You cannot mute a member with equal or higher role than yours.', + }); + return; + } + + if (!member.moderatable) { + await interaction.editReply({ + content: 'I do not have permission to mute this member.', + }); + return; + } + const durationMs = parseDuration(muteDuration); const maxTimeout = 28 * 24 * 60 * 60 * 1000; if (durationMs > maxTimeout) { - await interaction.reply({ + await interaction.editReply({ content: 'Timeout duration cannot exceed 28 days.', - flags: ['Ephemeral'], }); return; } @@ -94,19 +108,18 @@ const command: OptionsCommand = { guild: interaction.guild!, action: 'mute', target: member, - moderator: moderator!, + moderator, reason, duration: muteDuration, }); - await interaction.reply({ - content: `<@${member.id}> has been timed out for ${muteDuration}. Reason: ${reason}`, + await interaction.editReply({ + content: `<@${member.id}> has been muted for ${muteDuration}. Reason: ${reason}`, }); } catch (error) { console.error('Mute command error:', error); - await interaction.reply({ + await interaction.editReply({ content: 'Unable to timeout member.', - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/moderation/unban.ts b/src/commands/moderation/unban.ts index c160148..3778d96 100644 --- a/src/commands/moderation/unban.ts +++ b/src/commands/moderation/unban.ts @@ -20,52 +20,53 @@ const command: OptionsCommand = { .setRequired(true), ), execute: async (interaction) => { - const userId = interaction.options.get('userid')!.value as string; - const reason = interaction.options.get('reason')?.value as string; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - if ( - !interaction.memberPermissions?.has(PermissionsBitField.Flags.BanMembers) - ) { - await interaction.reply({ - content: 'You do not have permission to unban users.', - flags: ['Ephemeral'], - }); - return; - } + interaction.deferReply({ flags: ['Ephemeral'] }); try { + const userId = interaction.options.get('userid')?.value as string; + const reason = interaction.options.get('reason')?.value as string; + + if ( + !interaction.memberPermissions?.has( + PermissionsBitField.Flags.BanMembers, + ) + ) { + await interaction.editReply({ + content: 'You do not have permission to unban users.', + }); + return; + } try { - const ban = await interaction.guild?.bans.fetch(userId); + const ban = await interaction.guild.bans.fetch(userId); if (!ban) { - await interaction.reply({ + await interaction.editReply({ content: 'This user is not banned.', - flags: ['Ephemeral'], }); return; } } catch { - await interaction.reply({ + await interaction.editReply({ content: 'Error getting ban. Is this user banned?', - flags: ['Ephemeral'], }); return; } await executeUnban( interaction.client, - interaction.guildId!, + interaction.guild.id, userId, reason, ); - await interaction.reply({ + await interaction.editReply({ content: `<@${userId}> has been unbanned. Reason: ${reason}`, }); } catch (error) { console.error(error); - await interaction.reply({ + await interaction.editReply({ content: 'Unable to unban user.', - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index f9130f3..a52069f 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -20,43 +20,44 @@ const command: OptionsCommand = { .setRequired(true), ), execute: async (interaction) => { - const moderator = await interaction.guild?.members.fetch( - interaction.user.id, - ); - const member = await interaction.guild?.members.fetch( - interaction.options.get('member')!.value as string, - ); - const reason = interaction.options.get('reason')?.value as string; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - if ( - !interaction.memberPermissions?.has( - PermissionsBitField.Flags.ModerateMembers, - ) - ) { - await interaction.reply({ - content: 'You do not have permission to unmute members.', - flags: ['Ephemeral'], - }); - return; - } + await interaction.deferReply({ flags: ['Ephemeral'] }); try { + const moderator = await interaction.guild.members.fetch( + interaction.user.id, + ); + const member = await interaction.guild.members.fetch( + interaction.options.get('member')!.value as string, + ); + const reason = interaction.options.get('reason')?.value as string; + + if ( + !interaction.memberPermissions?.has( + PermissionsBitField.Flags.ModerateMembers, + ) + ) { + await interaction.editReply({ + content: 'You do not have permission to unmute members.', + }); + return; + } await executeUnmute( interaction.client, - interaction.guild!.id, - member!.id, + interaction.guild.id, + member.id, reason, moderator, ); - await interaction.reply({ - content: `<@${member!.id}>'s timeout has been removed. Reason: ${reason}`, + await interaction.editReply({ + content: `<@${member.id}>'s timeout has been removed. Reason: ${reason}`, }); } catch (error) { console.error('Unmute command error:', error); - await interaction.reply({ + await interaction.editReply({ content: 'Unable to unmute member.', - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 1eef588..ad0ad83 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -21,29 +21,38 @@ const command: OptionsCommand = { .setRequired(true), ), execute: async (interaction) => { - const moderator = await interaction.guild?.members.fetch( - interaction.user.id, - ); - const member = await interaction.guild?.members.fetch( - interaction.options.get('member')!.value as unknown as string, - ); - const reason = interaction.options.get('reason') - ?.value as unknown as string; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - if ( - !interaction.memberPermissions?.has( - PermissionsBitField.Flags.ModerateMembers, - ) || - moderator!.roles.highest.position <= member!.roles.highest.position - ) { - await interaction.reply({ - content: 'You do not have permission to warn this member.', - flags: ['Ephemeral'], - }); - return; - } + await interaction.deferReply({ flags: ['Ephemeral'] }); try { + const moderator = await interaction.guild.members.fetch( + interaction.user.id, + ); + const member = await interaction.guild.members.fetch( + interaction.options.get('member')!.value as unknown as string, + ); + const reason = interaction.options.getString('reason')!; + + if ( + !interaction.memberPermissions?.has( + PermissionsBitField.Flags.ModerateMembers, + ) + ) { + await interaction.editReply({ + content: 'You do not have permission to warn members.', + }); + return; + } + + if (moderator.roles.highest.position <= member.roles.highest.position) { + await interaction.editReply({ + content: + 'You cannot warn a member with equal or higher role than yours.', + }); + return; + } + await updateMemberModerationHistory({ discordId: member!.user.id, moderatorDiscordId: interaction.user.id, @@ -61,14 +70,13 @@ const command: OptionsCommand = { moderator: moderator!, reason: reason, }); - await interaction.reply( + await interaction.editReply( `<@${member!.user.id}> has been warned. Reason: ${reason}`, ); } catch (error) { console.error(error); - await interaction.reply({ + await interaction.editReply({ content: 'There was an error trying to warn the member.', - flags: ['Ephemeral'], }); } }, diff --git a/src/commands/testing/testJoin.ts b/src/commands/testing/testJoin.ts index cf66f6b..f812ae9 100644 --- a/src/commands/testing/testJoin.ts +++ b/src/commands/testing/testJoin.ts @@ -8,25 +8,27 @@ const command: Command = { .setDescription('Simulates a new member joining'), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; const guild = interaction.guild; + await interaction.deferReply({ flags: ['Ephemeral'] }); + if ( !interaction.memberPermissions!.has( PermissionsBitField.Flags.Administrator, ) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to use this command.', - flags: ['Ephemeral'], }); + return; } - const fakeMember = await guild!.members.fetch(interaction.user.id); - guild!.client.emit('guildMemberAdd', fakeMember); + const fakeMember = await guild.members.fetch(interaction.user.id); + guild.client.emit('guildMemberAdd', fakeMember); - await interaction.reply({ + await interaction.editReply({ content: 'Triggered the join event!', - flags: ['Ephemeral'], }); }, }; diff --git a/src/commands/testing/testLeave.ts b/src/commands/testing/testLeave.ts index 1aba95c..fae7e89 100644 --- a/src/commands/testing/testLeave.ts +++ b/src/commands/testing/testLeave.ts @@ -9,25 +9,26 @@ const command: Command = { .setDescription('Simulates a member leaving'), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; const guild = interaction.guild; + await interaction.deferReply({ flags: ['Ephemeral'] }); + if ( !interaction.memberPermissions!.has( PermissionsBitField.Flags.Administrator, ) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to use this command.', - flags: ['Ephemeral'], }); } - const fakeMember = await guild!.members.fetch(interaction.user.id); - guild!.client.emit('guildMemberRemove', fakeMember); + const fakeMember = await guild.members.fetch(interaction.user.id); + guild.client.emit('guildMemberRemove', fakeMember); - await interaction.reply({ + await interaction.editReply({ content: 'Triggered the leave event!', - flags: ['Ephemeral'], }); await updateMember({ diff --git a/src/commands/util/config.ts b/src/commands/util/config.ts index 9dde461..46d6a98 100644 --- a/src/commands/util/config.ts +++ b/src/commands/util/config.ts @@ -14,12 +14,15 @@ const command: Command = { .setDescription('(Admin Only) Display the current configuration') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + await interaction.deferReply({ flags: ['Ephemeral'] }); + if ( !interaction.memberPermissions?.has(PermissionFlagsBits.Administrator) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to use this command.', - flags: ['Ephemeral'], }); return; } @@ -180,10 +183,9 @@ const command: Command = { ? [createPaginationButtons(pages.length, currentPage)] : []; - const reply = await interaction.reply({ + const reply = await interaction.editReply({ embeds: [pages[currentPage]], components, - flags: ['Ephemeral'], }); if (pages.length <= 1) return; diff --git a/src/commands/util/help.ts b/src/commands/util/help.ts index 3ab08e3..d2a253c 100644 --- a/src/commands/util/help.ts +++ b/src/commands/util/help.ts @@ -26,9 +26,11 @@ const command: OptionsCommand = { ), execute: async (interaction) => { - try { - await interaction.deferReply(); + if (!interaction.isChatInputCommand() || !interaction.guild) return; + await interaction.deferReply(); + + try { const client = interaction.client as ExtendedClient; const commandName = interaction.options.getString('command'); @@ -178,7 +180,7 @@ async function handleSpecificCommand( const cmd = client.commands.get(commandName); if (!cmd) { - return interaction.reply({ + return interaction.editReply({ content: `Command \`${commandName}\` not found.`, ephemeral: true, }); @@ -227,7 +229,7 @@ async function handleSpecificCommand( inline: false, }); - return interaction.reply({ embeds: [embed] }); + return interaction.editReply({ embeds: [embed] }); } /** diff --git a/src/commands/util/members.ts b/src/commands/util/members.ts index 6dd4762..c76ec28 100644 --- a/src/commands/util/members.ts +++ b/src/commands/util/members.ts @@ -16,6 +16,10 @@ const command: Command = { .setName('members') .setDescription('Lists all non-bot members of the server'), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + await interaction.deferReply(); + let members = await getAllMembers(); members = members.sort((a, b) => (a.discordUsername ?? '').localeCompare(b.discordUsername ?? ''), @@ -63,7 +67,7 @@ const command: Command = { const components = pages.length > 1 ? [getButtonActionRow(), getSelectMenuRow()] : []; - await interaction.reply({ + await interaction.editReply({ embeds: [pages[currentPage]], components, }); diff --git a/src/commands/util/ping.ts b/src/commands/util/ping.ts index 5e2bddc..3bf1894 100644 --- a/src/commands/util/ping.ts +++ b/src/commands/util/ping.ts @@ -8,7 +8,7 @@ const command: Command = { .setDescription('Check the latency from you to the bot'), execute: async (interaction) => { await interaction.reply( - `Pong! Latency: ${Date.now() - interaction.createdTimestamp}ms`, + `🏓 Pong! Latency: ${Date.now() - interaction.createdTimestamp}ms`, ); }, }; diff --git a/src/commands/util/recalculatelevels.ts b/src/commands/util/recalculatelevels.ts index 4c30806..8653345 100644 --- a/src/commands/util/recalculatelevels.ts +++ b/src/commands/util/recalculatelevels.ts @@ -8,21 +8,22 @@ const command: Command = { .setName('recalculatelevels') .setDescription('(Admin Only) Recalculate all user levels'), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + await interaction.deferReply({ flags: ['Ephemeral'] }); + await interaction.editReply('Recalculating levels...'); + if ( !interaction.memberPermissions?.has( PermissionsBitField.Flags.Administrator, ) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to use this command.', - flags: ['Ephemeral'], }); return; } - await interaction.deferReply(); - await interaction.editReply('Recalculating levels...'); - try { await recalculateUserLevels(); await interaction.editReply('Levels recalculated successfully!'); diff --git a/src/commands/util/reconnect.ts b/src/commands/util/reconnect.ts index 886fcca..a86d248 100644 --- a/src/commands/util/reconnect.ts +++ b/src/commands/util/reconnect.ts @@ -36,7 +36,9 @@ const command: SubcommandCommand = { ), execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + await interaction.deferReply({ flags: ['Ephemeral'] }); const config = loadConfig(); const managerRoleId = config.roles.staffRoles.find( @@ -52,18 +54,15 @@ const command: SubcommandCommand = { PermissionsBitField.Flags.Administrator, ) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to use this command. This command is restricted to users with the Manager role.', - flags: ['Ephemeral'], }); return; } const subcommand = interaction.options.getSubcommand(); - await interaction.deferReply({ flags: ['Ephemeral'] }); - try { if (subcommand === 'database') { await handleDatabaseReconnect(interaction); diff --git a/src/commands/util/restart.ts b/src/commands/util/restart.ts index 31ecfc9..a8dbc91 100644 --- a/src/commands/util/restart.ts +++ b/src/commands/util/restart.ts @@ -18,6 +18,10 @@ const command: Command = { .setName('restart') .setDescription('(Manager Only) Restart the bot'), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + await interaction.deferReply({ flags: ['Ephemeral'] }); + const config = loadConfig(); const managerRoleId = config.roles.staffRoles.find( (role) => role.name === 'Manager', @@ -32,17 +36,15 @@ const command: Command = { PermissionsBitField.Flags.Administrator, ) ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to restart the bot. This command is restricted to users with the Manager role.', - flags: ['Ephemeral'], }); return; } - await interaction.reply({ + await interaction.editReply({ content: 'Restarting the bot... This may take a few moments.', - flags: ['Ephemeral'], }); const dbConnected = await ensureDatabaseConnection(); diff --git a/src/commands/util/server.ts b/src/commands/util/server.ts index 766c9f0..691cca0 100644 --- a/src/commands/util/server.ts +++ b/src/commands/util/server.ts @@ -7,8 +7,10 @@ const command: Command = { .setName('server') .setDescription('Provides information about the server.'), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + await interaction.reply( - `The server **${interaction!.guild!.name}** has **${interaction!.guild!.memberCount}** members and was created on **${interaction!.guild!.createdAt}**. It is **${new Date().getFullYear() - interaction!.guild!.createdAt.getFullYear()!}** years old.`, + `The server **${interaction.guild.name}** has **${interaction.guild.memberCount}** members and was created on **${interaction.guild.createdAt}**. It is **${new Date().getFullYear() - interaction.guild.createdAt.getFullYear()}** years old.`, ); }, }; diff --git a/src/commands/util/user-info.ts b/src/commands/util/user-info.ts index 7b0a52b..9a222dd 100644 --- a/src/commands/util/user-info.ts +++ b/src/commands/util/user-info.ts @@ -19,13 +19,17 @@ const command: OptionsCommand = { .setRequired(true), ), execute: async (interaction) => { + if (!interaction.isChatInputCommand() || !interaction.guild) return; + + await interaction.deferReply(); + const userOption = interaction.options.get( 'user', ) as unknown as GuildMember; const user = userOption.user; if (!userOption || !user) { - await interaction.reply('User not found'); + await interaction.editReply('User not found'); return; } if ( @@ -33,7 +37,7 @@ const command: OptionsCommand = { PermissionsBitField.Flags.ModerateMembers, ) ) { - await interaction.reply( + await interaction.editReply( 'You do not have permission to view member information.', ); return; @@ -140,7 +144,7 @@ const command: OptionsCommand = { iconURL: interaction.user.displayAvatarURL(), }); - await interaction.reply({ embeds: [embed] }); + await interaction.editReply({ embeds: [embed] }); }, }; diff --git a/src/commands/util/xp.ts b/src/commands/util/xp.ts index 979da6a..3a667c8 100644 --- a/src/commands/util/xp.ts +++ b/src/commands/util/xp.ts @@ -71,12 +71,16 @@ const command: SubcommandCommand = { ), ), execute: async (interaction) => { - if (!interaction.isChatInputCommand()) return; + if (!interaction.isChatInputCommand() || !interaction.guild) return; - const commandUser = interaction.guild?.members.cache.get( + const commandUser = interaction.guild.members.cache.get( interaction.user.id, ); + await interaction.deferReply({ + flags: ['Ephemeral'], + }); + const config = loadConfig(); const managerRoleId = config.roles.staffRoles.find( (role) => role.name === 'Manager', @@ -87,18 +91,12 @@ const command: SubcommandCommand = { !managerRoleId || commandUser.roles.highest.comparePositionTo(managerRoleId) < 0 ) { - await interaction.reply({ + await interaction.editReply({ content: 'You do not have permission to use this command', - flags: ['Ephemeral'], }); return; } - await interaction.deferReply({ - flags: ['Ephemeral'], - }); - await interaction.editReply('Processing...'); - const subcommand = interaction.options.getSubcommand(); const user = interaction.options.getUser('user', true); const amount = interaction.options.getInteger('amount', false); diff --git a/src/discord-bot.ts b/src/discord-bot.ts index f754db7..0183107 100644 --- a/src/discord-bot.ts +++ b/src/discord-bot.ts @@ -12,9 +12,10 @@ async function startBot() { GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildModeration, + GatewayIntentBits.GuildInvites, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.GuildModeration, ], }, config, diff --git a/src/structures/ExtendedClient.ts b/src/structures/ExtendedClient.ts index aa28e65..d56e002 100644 --- a/src/structures/ExtendedClient.ts +++ b/src/structures/ExtendedClient.ts @@ -33,6 +33,10 @@ export class ExtendedClient extends Client { console.log('Skipping command deployment (SKIP_COMMAND_DEPLOY=true)'); const commandFiles = await this.loadCommandsWithoutDeploying(); + if (!commandFiles?.length) { + throw new Error('No commands found'); + } + await registerEvents(this); console.log( `Loaded ${commandFiles.length} commands and registered events (without deployment)`, diff --git a/src/types/ConfigTypes.ts b/src/types/ConfigTypes.ts index b38b126..90418b9 100644 --- a/src/types/ConfigTypes.ts +++ b/src/types/ConfigTypes.ts @@ -5,6 +5,7 @@ export interface Config { token: string; clientId: string; guildId: string; + serverInvite: string; database: { dbConnectionString: string; maxRetryAttempts: number;