From 2ec7c73709f4c660e0f0df7a977b5a042b4cc795 Mon Sep 17 00:00:00 2001 From: Ayden Jahola Date: Thu, 5 Sep 2024 03:50:51 +0100 Subject: [PATCH] restructure commands to be in seperate dirs, add mod commands and plenty others --- commands/information/botinfo.js | 57 +++++++++++ commands/information/serverinfo.js | 55 ++++++++++ commands/moderation/purge.js | 140 ++++++++++++++++++++++++++ commands/moderation/userinfo.js | 71 +++++++++++++ commands/moderation/warn.js | 117 +++++++++++++++++++++ commands/utility/help.js | 60 +++++++++++ commands/utility/ping.js | 44 ++++++++ commands/utility/uptime.js | 46 +++++++++ commands/{ => verification}/code.js | 4 +- commands/{ => verification}/verify.js | 2 +- index.js | 45 ++++++--- models/warning.js | 10 ++ 12 files changed, 635 insertions(+), 16 deletions(-) create mode 100644 commands/information/botinfo.js create mode 100644 commands/information/serverinfo.js create mode 100644 commands/moderation/purge.js create mode 100644 commands/moderation/userinfo.js create mode 100644 commands/moderation/warn.js create mode 100644 commands/utility/help.js create mode 100644 commands/utility/ping.js create mode 100644 commands/utility/uptime.js rename commands/{ => verification}/code.js (93%) rename commands/{ => verification}/verify.js (95%) create mode 100644 models/warning.js diff --git a/commands/information/botinfo.js b/commands/information/botinfo.js new file mode 100644 index 0000000..8a1562b --- /dev/null +++ b/commands/information/botinfo.js @@ -0,0 +1,57 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("botinfo") + .setDescription("Displays information about the bot"), + + async execute(interaction, client) { + try { + const uptime = client.uptime; // Uptime in milliseconds + const days = Math.floor(uptime / (24 * 60 * 60 * 1000)); + const hours = Math.floor( + (uptime % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000) + ); + const minutes = Math.floor((uptime % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((uptime % (60 * 1000)) / 1000); + + const botInfoEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Bot Information") + .addFields( + { name: "Bot Name", value: client.user.tag, inline: true }, + { + name: "Uptime", + value: `${days} days, ${hours} hours, ${minutes} minutes, ${seconds} seconds`, + inline: true, + }, + { + name: "Server Count", + value: `${client.guilds.cache.size}`, + inline: true, + }, + { + name: "User Count", + value: `${client.users.cache.size}`, + inline: true, + } + ) + .setThumbnail(client.user.displayAvatarURL()) + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + await interaction.reply({ + embeds: [botInfoEmbed], + }); + } catch (error) { + console.error("Error executing botinfo command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/information/serverinfo.js b/commands/information/serverinfo.js new file mode 100644 index 0000000..091a579 --- /dev/null +++ b/commands/information/serverinfo.js @@ -0,0 +1,55 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("serverinfo") + .setDescription("Displays information about the server"), + + async execute(interaction) { + try { + const guild = interaction.guild; + const serverInfoEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Server Information") + .addFields( + { name: "Server Name", value: guild.name, inline: true }, + { + name: "Total Members", + value: `${guild.memberCount}`, + inline: true, + }, + { + name: "Created On", + value: guild.createdAt.toDateString(), + inline: true, + }, + { + name: "Region", + value: guild.preferredLocale || "Unknown", + inline: true, + }, // Fallback to "Unknown" + { + name: "Verification Level", + value: guild.verificationLevel.toString(), + inline: true, + } // Convert enum to string + ) + .setThumbnail(guild.iconURL()) + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + await interaction.reply({ + embeds: [serverInfoEmbed], + }); + } catch (error) { + console.error("Error executing serverinfo command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/moderation/purge.js b/commands/moderation/purge.js new file mode 100644 index 0000000..1a05a2f --- /dev/null +++ b/commands/moderation/purge.js @@ -0,0 +1,140 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("purge") + .setDescription("Deletes messages from the channel") + .addStringOption((option) => + option + .setName("type") + .setDescription("Type of purge operation") + .setRequired(true) + .addChoices( + { name: "Purge Specific Number", value: "specific" }, + { name: "Purge All", value: "all" } + ) + ) + .addIntegerOption((option) => + option + .setName("amount") + .setDescription("The number of messages to delete (1-100)") + .setRequired(false) + .setMinValue(1) + .setMaxValue(100) + ), + isModOnly: true, + + async execute(interaction) { + try { + const requiredRoleId = process.env.MOD_ROLE_ID; + if (!interaction.member.roles.cache.has(requiredRoleId)) { + await interaction.reply({ + content: "You do not have the required role to use this command!", + ephemeral: true, + }); + return; + } + + const type = interaction.options.getString("type"); + let amount = interaction.options.getInteger("amount"); + const logChannelId = process.env.LOG_CHANNEL_ID; + const logChannel = interaction.guild.channels.cache.get(logChannelId); + + if (type === "specific") { + // Ensure the number of messages to delete is between 1 and 100 + if (amount < 1 || amount > 100) { + await interaction.reply({ + content: "Please specify a number between 1 and 100.", + ephemeral: true, + }); + return; + } + + // Delete a specific number of messages + const fetchedMessages = await interaction.channel.messages.fetch({ + limit: amount, + }); + await interaction.channel.bulkDelete(fetchedMessages); + + const purgeEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Messages Purged") + .setDescription(`Successfully deleted ${amount} messages.`) + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + // Send confirmation as ephemeral message + await interaction.reply({ + embeds: [purgeEmbed], + ephemeral: true, + }); + + // Send log to the logs channel + if (logChannel) { + const logEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Purge Operation") + .setDescription( + `User ${interaction.user.tag} purged ${amount} messages from ${interaction.channel.name}.` + ) + .setTimestamp(); + + await logChannel.send({ embeds: [logEmbed] }); + } + } else if (type === "all") { + // Purge all messages (up to 100 messages at a time) + let messages; + let deletedMessagesCount = 0; + do { + messages = await interaction.channel.messages.fetch({ limit: 100 }); + if (messages.size === 0) break; + deletedMessagesCount += messages.size; + await interaction.channel.bulkDelete(messages); + } while (messages.size >= 2); // Keep fetching and deleting until no more messages are left + + const purgeEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("All Messages Purged") + .setDescription("Successfully deleted all messages in the channel.") + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + // Send confirmation as ephemeral message + await interaction.reply({ + embeds: [purgeEmbed], + ephemeral: true, + }); + + // Send log to the logs channel + if (logChannel) { + const logEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("Purge Operation") + .setDescription( + `User ${interaction.user.tag} purged all messages from ${interaction.channel.name}. Total messages deleted: ${deletedMessagesCount}.` + ) + .setTimestamp(); + + await logChannel.send({ embeds: [logEmbed] }); + } + } + } catch (error) { + console.error("Error executing purge command:", error); + + try { + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } catch (replyError) { + console.error("Error replying to interaction:", replyError); + } + } + }, +}; diff --git a/commands/moderation/userinfo.js b/commands/moderation/userinfo.js new file mode 100644 index 0000000..94fcada --- /dev/null +++ b/commands/moderation/userinfo.js @@ -0,0 +1,71 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("userinfo") + .setDescription("Displays information about a user") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to get information about") + .setRequired(false) + ), + isModOnly: true, + + async execute(interaction) { + try { + const requiredRoleId = process.env.MOD_ROLE_ID; + if (!interaction.member.roles.cache.has(requiredRoleId)) { + await interaction.reply({ + content: "You do not have the required role to use this command!", + ephemeral: true, + }); + return; + } + + const user = interaction.options.getUser("user") || interaction.user; + const member = interaction.guild.members.cache.get(user.id); + + // Create the embed for user information + const userInfoEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("User Information") + .addFields( + { name: "Username", value: user.tag, inline: true }, + { + name: "Joined Server On", + value: member ? member.joinedAt.toDateString() : "N/A", + inline: true, + }, + { + name: "Account Created On", + value: user.createdAt.toDateString(), + inline: true, + }, + { + name: "Roles", + value: member + ? member.roles.cache.map((role) => role.name).join(", ") + : "N/A", + inline: false, + } + ) + .setThumbnail(user.displayAvatarURL()) + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + await interaction.reply({ + embeds: [userInfoEmbed], + }); + } catch (error) { + console.error("Error executing userinfo command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/moderation/warn.js b/commands/moderation/warn.js new file mode 100644 index 0000000..9449e7d --- /dev/null +++ b/commands/moderation/warn.js @@ -0,0 +1,117 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const Warning = require("../../models/warning"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("warn") + .setDescription("Issue a warning to a user") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to warn") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("The reason for the warning") + .setRequired(true) + ), + isModOnly: true, + + async execute(interaction) { + try { + const requiredRoleId = process.env.MOD_ROLE_ID; + if (!interaction.member.roles.cache.has(requiredRoleId)) { + await interaction.reply({ + content: "You do not have the required role to use this command!", + ephemeral: true, + }); + return; + } + + const user = interaction.options.getUser("user"); + const reason = interaction.options.getString("reason"); + const member = interaction.guild.members.cache.get(user.id); + + // Save the warning to the database + const warning = new Warning({ + userId: user.id, + guildId: interaction.guild.id, + reason: reason, + }); + + await warning.save(); + + const logChannelId = process.env.LOG_CHANNEL_ID; + const logChannel = interaction.guild.channels.cache.get(logChannelId); + + if (!logChannel) { + await interaction.reply({ + content: "Log channel not found!", + ephemeral: true, + }); + return; + } + + // Create and send the warning log to the log channel + const warnEmbed = new EmbedBuilder() + .setColor("#ffcc00") + .setTitle("User Warned") + .addFields( + { name: "User", value: `${user.tag} (${user.id})`, inline: true }, + { name: "Reason", value: reason, inline: true }, + { name: "Issued By", value: interaction.user.tag, inline: true }, + { name: "Date", value: new Date().toLocaleString(), inline: true } + ) + .setTimestamp() + .setFooter({ + text: `Warned in ${interaction.guild.name}`, + iconURL: interaction.guild.iconURL(), + }); + + await logChannel.send({ embeds: [warnEmbed] }); + + // Send a DM to the user + try { + const dmEmbed = new EmbedBuilder() + .setColor("#ffcc00") + .setTitle("Warning Notice") + .setDescription( + `You have been warned in **${interaction.guild.name}**.` + ) + .addFields( + { name: "Reason", value: reason, inline: false }, + { name: "Issued By", value: interaction.user.tag, inline: false }, + { name: "Date", value: new Date().toLocaleString(), inline: false } + ) + .setFooter({ + text: `Please follow the rules of ${interaction.guild.name}`, + iconURL: interaction.guild.iconURL(), + }) + .setTimestamp(); + + await user.send({ embeds: [dmEmbed] }); + } catch (dmError) { + console.error("Error sending DM to user:", dmError); + await interaction.reply({ + content: `Failed to send a DM to ${user.tag}.`, + ephemeral: true, + }); + return; + } + + // Notify the mod who issued the warning + await interaction.reply({ + content: `Successfully warned ${user.tag} for: ${reason}`, + ephemeral: true, + }); + } catch (error) { + console.error("Error executing warn command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/utility/help.js b/commands/utility/help.js new file mode 100644 index 0000000..89c2c61 --- /dev/null +++ b/commands/utility/help.js @@ -0,0 +1,60 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("help") + .setDescription("Lists all available commands"), + + async execute(interaction, client) { + try { + const modRoleId = process.env.MOD_ROLE_ID; + const isMod = interaction.member.roles.cache.has(modRoleId); + + const serverName = interaction.guild.name; + + const helpEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Available Commands") + .setDescription("Here are all the available commands:") + .setTimestamp() + .setFooter({ + text: `${serverName}`, + iconURL: client.user.displayAvatarURL(), + }); + + // Add general commands + client.commands.forEach((command) => { + if (!command.isModOnly) { + helpEmbed.addFields({ + name: `/${command.data.name}`, + value: command.data.description, + inline: false, + }); + } + }); + + // Add mod-only commands if the user is a mod + if (isMod) { + client.commands.forEach((command) => { + if (command.isModOnly) { + helpEmbed.addFields({ + name: `/${command.data.name}`, + value: command.data.description + " (Mods only)", + inline: false, + }); + } + }); + } + + await interaction.reply({ + embeds: [helpEmbed], + }); + } catch (error) { + console.error("Error executing the help command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/utility/ping.js b/commands/utility/ping.js new file mode 100644 index 0000000..4b962da --- /dev/null +++ b/commands/utility/ping.js @@ -0,0 +1,44 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("ping") + .setDescription("Replies with Pong! and bot latency"), + + async execute(interaction, client) { + try { + const sent = await interaction.reply({ + content: "Pinging...", + fetchReply: true, + }); + + const latency = sent.createdTimestamp - interaction.createdTimestamp; + const apiLatency = Math.round(client.ws.ping); + const serverName = interaction.guild.name; + + const pingEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Pong! 🏓") + .setDescription("Bot latency information:") + .addFields( + { name: "Latency", value: `${latency} ms`, inline: true }, + { name: "API Latency", value: `${apiLatency} ms`, inline: true } + ) + .setTimestamp() + .setFooter({ + text: `${serverName}`, + iconURL: client.user.displayAvatarURL(), + }); + + await interaction.editReply({ + embeds: [pingEmbed], + }); + } catch (error) { + console.error("Error executing the ping command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/utility/uptime.js b/commands/utility/uptime.js new file mode 100644 index 0000000..88544f9 --- /dev/null +++ b/commands/utility/uptime.js @@ -0,0 +1,46 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("uptime") + .setDescription("Shows how long the bot has been running"), + + async execute(interaction, client) { + try { + const uptime = client.uptime; + const days = Math.floor(uptime / (24 * 60 * 60 * 1000)); + const hours = Math.floor( + (uptime % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000) + ); + const minutes = Math.floor((uptime % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((uptime % (60 * 1000)) / 1000); + const serverName = interaction.guild.name; + + const uptimeEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Bot Uptime") + .setDescription(`The bot has been online for:`) + .addFields( + { name: "Days", value: `${days}`, inline: true }, + { name: "Hours", value: `${hours}`, inline: true }, + { name: "Minutes", value: `${minutes}`, inline: true }, + { name: "Seconds", value: `${seconds}`, inline: true } + ) + .setTimestamp() + .setFooter({ + text: `${serverName}`, + iconURL: client.user.displayAvatarURL(), + }); + + await interaction.reply({ + embeds: [uptimeEmbed], + }); + } catch (error) { + console.error("Error executing the uptime command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/code.js b/commands/verification/code.js similarity index 93% rename from commands/code.js rename to commands/verification/code.js index aa83695..d0162b1 100644 --- a/commands/code.js +++ b/commands/verification/code.js @@ -1,5 +1,5 @@ const { SlashCommandBuilder } = require("discord.js"); -const VerificationCode = require("../models/VerificationCode"); +const VerificationCode = require("../../models/VerificationCode"); module.exports = { data: new SlashCommandBuilder() @@ -89,7 +89,7 @@ module.exports = { // Get the admin log channel const adminLogChannel = client.channels.cache.get( - process.env.ADMIN_LOG_CHANNEL_ID + process.env.LOG_CHANNEL_ID ); if (adminLogChannel) { diff --git a/commands/verify.js b/commands/verification/verify.js similarity index 95% rename from commands/verify.js rename to commands/verification/verify.js index 0e8ee8c..a9aa304 100644 --- a/commands/verify.js +++ b/commands/verification/verify.js @@ -1,6 +1,6 @@ const { SlashCommandBuilder } = require("discord.js"); const nodemailer = require("nodemailer"); -const VerificationCode = require("../models/VerificationCode"); +const VerificationCode = require("../../models/VerificationCode"); const transporter = nodemailer.createTransport({ service: "Gmail", diff --git a/index.js b/index.js index 431a2a4..3071b35 100644 --- a/index.js +++ b/index.js @@ -18,19 +18,31 @@ const client = new Client({ ], }); -const GUIlD_ID = process.env.GUILD_ID; +const GUILD_ID = process.env.GUILD_ID; client.commands = new Collection(); -const commandFiles = fs - .readdirSync(path.join(__dirname, "commands")) - .filter((file) => file.endsWith(".js")); +// Function to recursively read commands from subdirectories +function loadCommands(dir) { + const files = fs.readdirSync(dir); -for (const file of commandFiles) { - const command = require(`./commands/${file}`); - client.commands.set(command.data.name, command); + for (const file of files) { + const filePath = path.join(dir, file); + + if (fs.statSync(filePath).isDirectory()) { + // If it's a directory, recurse into it + loadCommands(filePath); + } else if (file.endsWith(".js")) { + // If it's a JavaScript file, load the command + const command = require(filePath); + client.commands.set(command.data.name, command); + } + } } +// Load all commands from the commands directory and its subdirectories +loadCommands(path.join(__dirname, "commands")); + client.once("ready", async () => { console.log(`\n==============================`); console.log(`🤖 Logged in as ${client.user.tag}`); @@ -52,7 +64,7 @@ client.once("ready", async () => { const rest = new REST({ version: "10" }).setToken(process.env.BOT_TOKEN); try { - await rest.put(Routes.applicationGuildCommands(client.user.id, GUIlD_ID), { + await rest.put(Routes.applicationGuildCommands(client.user.id, GUILD_ID), { body: commands, }); console.log("Successfully registered all application commands."); @@ -77,11 +89,18 @@ client.on("interactionCreate", async (interaction) => { try { await command.execute(interaction, client); } catch (err) { - console.error(err); - await interaction.reply({ - content: "There was an error while executing this command!", - ephemeral: true, - }); + console.error("Error executing command:", err); + if (interaction.deferred || interaction.ephemeral) { + await interaction.followUp({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } else { + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } } }); diff --git a/models/warning.js b/models/warning.js new file mode 100644 index 0000000..b0337e9 --- /dev/null +++ b/models/warning.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); + +const warningSchema = new mongoose.Schema({ + userId: { type: String, required: true }, + guildId: { type: String, required: true }, + reason: { type: String, required: true }, + date: { type: Date, default: Date.now }, +}); + +module.exports = mongoose.model("Warning", warningSchema);