From 1a3599d8b047672f832b8f7bf313d56b5f4beae4 Mon Sep 17 00:00:00 2001 From: Ayden Jahola Date: Sun, 8 Sep 2024 21:58:36 +0100 Subject: [PATCH] modertaion: add ban, kick and timeout commands --- commands/moderation/ban.js | 148 +++++++++++++++++++++++++ commands/moderation/kick.js | 136 +++++++++++++++++++++++ commands/moderation/timeout.js | 191 +++++++++++++++++++++++++++++++++ models/BannedUser.js | 14 +++ models/KickedUser.js | 14 +++ models/TimedOutUser.js | 15 +++ 6 files changed, 518 insertions(+) create mode 100644 commands/moderation/ban.js create mode 100644 commands/moderation/kick.js create mode 100644 commands/moderation/timeout.js create mode 100644 models/BannedUser.js create mode 100644 models/KickedUser.js create mode 100644 models/TimedOutUser.js diff --git a/commands/moderation/ban.js b/commands/moderation/ban.js new file mode 100644 index 0000000..9830824 --- /dev/null +++ b/commands/moderation/ban.js @@ -0,0 +1,148 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const BannedUser = require("../../models/BannedUser"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("ban") + .setDescription("Bans a user from the server") + .addUserOption((option) => + option.setName("user").setDescription("The user to ban").setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("Reason for banning the user") + .setRequired(true) + ), + isModOnly: true, + + async execute(interaction) { + let replySent = false; + + 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, + }); + replySent = true; + return; + } + + const user = interaction.options.getUser("user"); + const reason = + interaction.options.getString("reason") || "No reason provided"; + + // Fetch the member object from the guild + const member = await interaction.guild.members.fetch(user.id); + + if (!member) { + await interaction.reply({ + content: "User not found in the guild!", + ephemeral: true, + }); + replySent = true; + return; + } + + // Check if the bot can ban the member + if ( + member.roles.highest.position >= + interaction.guild.members.me.roles.highest.position + ) { + await interaction.reply({ + content: + "I cannot ban this user as they have a higher or equal role than me!", + ephemeral: true, + }); + replySent = true; + return; + } + + // Ban the user + await member.ban({ reason }); + + // Save banned user to MongoDB + await BannedUser.create({ + bannedUserId: user.id, + bannedUserTag: user.tag, + bannerId: interaction.user.id, + bannerTag: interaction.user.tag, + reason: reason, + }); + + // Send DM to the banned user + try { + await user.send({ + content: `You have been banned from the server. Reason: ${reason}`, + }); + } catch (dmError) { + console.error(`Error sending DM to ${user.tag}:`, dmError); + // Only reply with this if the interaction hasn't been replied to yet + if (!replySent) { + await interaction.reply({ + content: `Failed to send a DM to ${user.tag}. They might have DMs disabled.`, + ephemeral: true, + }); + replySent = true; + } + } + + const banEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("User Banned") + .setDescription( + `${user.tag} has been banned from the server.\nReason: ${reason}` + ) + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + // Send confirmation as ephemeral message + if (!replySent) { + await interaction.reply({ + embeds: [banEmbed], + ephemeral: true, + }); + replySent = true; + } + + // log the ban in a designated channel + const logChannelId = process.env.LOG_CHANNEL_ID; + const logChannel = interaction.guild.channels.cache.get(logChannelId); + + if (logChannel) { + const logEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("User Banned") + .setDescription( + `${user.tag} was banned from the server. Reason: ${reason}` + ) + .setFooter({ + text: `Banned by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(); + + await logChannel.send({ embeds: [logEmbed] }); + } + } catch (error) { + console.error("Error executing ban command:", error); + + // If the interaction hasn't been replied to yet, reply with an error message + if (!replySent) { + 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/kick.js b/commands/moderation/kick.js new file mode 100644 index 0000000..458251b --- /dev/null +++ b/commands/moderation/kick.js @@ -0,0 +1,136 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const KickedUser = require("../../models/KickedUser"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("kick") + .setDescription("Kicks a user from the server") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to kick") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("Reason for kicking the user") + .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") || "No reason provided"; + + // Fetch the member object from the guild + const member = await interaction.guild.members.fetch(user.id); + + if (!member) { + await interaction.reply({ + content: "User not found in the guild!", + ephemeral: true, + }); + return; + } + + // Check if the bot can kick the member + if ( + member.roles.highest.position >= + interaction.guild.members.me.roles.highest.position + ) { + await interaction.reply({ + content: + "I cannot kick this user as they have a higher or equal role than me!", + ephemeral: true, + }); + return; + } + + // Kick the user + await member.kick(reason); + + // Save kicked user to MongoDB + await KickedUser.create({ + kickedUserId: user.id, + kickedUserTag: user.tag, + kickerId: interaction.user.id, + kickerTag: interaction.user.tag, + reason: reason, + }); + + // Send DM to the kicked user + try { + await user.send({ + content: `You have been kicked from the server. Reason: ${reason}`, + }); + } catch (dmError) { + console.error(`Error sending DM to ${user.tag}:`, dmError); + // If DM fails, make sure to handle it properly + // Avoid using followUp here if interaction has already been replied to + } + + const kickEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("User Kicked") + .setDescription( + `${user.tag} has been kicked from the server.\nReason: ${reason}` + ) + .setTimestamp() + .setFooter({ + text: `Requested by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }); + + // Send confirmation as ephemeral message + await interaction.reply({ + embeds: [kickEmbed], + ephemeral: true, + }); + + // log the kick in a designated channel + const logChannelId = process.env.LOG_CHANNEL_ID; + const logChannel = interaction.guild.channels.cache.get(logChannelId); + + if (logChannel) { + const logEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("User Kicked") + .setDescription( + `${user.tag} was kicked from the server. Reason: ${reason}` + ) + .setFooter({ + text: `Kicked by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(); + + await logChannel.send({ embeds: [logEmbed] }); + } + } catch (error) { + console.error("Error executing kick command:", error); + + try { + if (!interaction.replied) { + 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/timeout.js b/commands/moderation/timeout.js new file mode 100644 index 0000000..be83431 --- /dev/null +++ b/commands/moderation/timeout.js @@ -0,0 +1,191 @@ +const { + SlashCommandBuilder, + EmbedBuilder, + PermissionsBitField, +} = require("discord.js"); +const TimedOutUser = require("../../models/TimedOutUser"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("timeout") + .setDescription("Timeout a user in the server") + .addUserOption((option) => + option + .setName("user") + .setDescription("The user to timeout") + .setRequired(true) + ) + .addIntegerOption((option) => + option + .setName("duration") + .setDescription("Duration of the timeout in minutes") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("reason") + .setDescription("Reason for the timeout") + .setRequired(true) + ), + isModOnly: true, + + async execute(interaction) { + let replySent = false; + + 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, + }); + replySent = true; + return; + } + + const user = interaction.options.getUser("user"); + const duration = interaction.options.getInteger("duration"); + const reason = + interaction.options.getString("reason") || "No reason provided"; + + // Fetch the member object from the guild + const member = await interaction.guild.members.fetch(user.id); + + if (!member) { + await interaction.reply({ + content: "User not found in the guild!", + ephemeral: true, + }); + replySent = true; + return; + } + + // Check if the bot has permission to manage roles + if ( + !interaction.guild.members.me.permissions.has( + PermissionsBitField.Flags.ManageRoles + ) + ) { + await interaction.reply({ + content: "I do not have permission to manage roles!", + ephemeral: true, + }); + replySent = true; + return; + } + + // Ensure the bot's role is high enough to apply the timeout + if ( + member.roles.highest.position >= + interaction.guild.members.me.roles.highest.position + ) { + await interaction.reply({ + content: + "I cannot timeout this user as they have a higher or equal role than me!", + ephemeral: true, + }); + replySent = true; + return; + } + + // Create a timeout role or use an existing one + let timeoutRole = interaction.guild.roles.cache.find( + (role) => role.name === "Timed Out" + ); + if (!timeoutRole) { + timeoutRole = await interaction.guild.roles.create({ + name: "Timed Out", + color: "#ff0000", + permissions: [], + }); + interaction.guild.channels.cache.each(async (channel) => { + await channel.permissionOverwrites.edit(timeoutRole, { + SendMessages: false, + }); + }); + } + + // Apply the timeout + await member.roles.add(timeoutRole); + const timeoutEnd = Date.now() + duration * 60 * 1000; + + // Save timed out user to MongoDB + await TimedOutUser.create({ + timedOutUserId: user.id, + timedOutUserTag: user.tag, + moderatorId: interaction.user.id, + moderatorTag: interaction.user.tag, + reason: reason, + timeoutEnd: timeoutEnd, + }); + + // Inform the user of the timeout + try { + await user.send({ + content: `Hello ${user.username},\n\nWe wanted to inform you that a timeout has been applied to your account in **${interaction.guild.name}**. This action was taken to ensure the community remains respectful and enjoyable for everyone.\n\n**Reason:** ${reason}\n**Duration:** ${duration} minutes\n\nPlease use this time to review our community guidelines, and we look forward to welcoming you back after the timeout ends.\n\nIf you have any questions or concerns, feel free to reach out to the moderation team once your timeout is over.\n\nThank you for understanding, and we appreciate your cooperation.\n\nBest regards,\n**${interaction.guild.name}** Moderation Team`, + }); + } catch (dmError) { + console.error(`Error sending DM to ${user.tag}:`, dmError); + // Inform mod about the DM failure + if (!replySent) { + await interaction.reply({ + content: `Failed to send a DM to ${user.tag}. They might have DMs disabled.`, + ephemeral: true, + }); + replySent = true; + } + } + + const timeoutEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("User Timed Out") + .setDescription( + `${user.tag} has been timed out in the server.\nReason: ${reason}\nDuration: ${duration} minutes.` + ) + .setTimestamp(); + + // Send confirmation as ephemeral message + if (!replySent) { + await interaction.reply({ + embeds: [timeoutEmbed], + ephemeral: true, + }); + replySent = true; + } + + // log the timeout in a designated channel + const logChannelId = process.env.LOG_CHANNEL_ID; + const logChannel = interaction.guild.channels.cache.get(logChannelId); + + if (logChannel) { + const logEmbed = new EmbedBuilder() + .setColor("#ff0000") + .setTitle("User Timed Out") + .setDescription( + `${user.tag} was timed out in the server. Reason: ${reason}\nDuration: ${duration} minutes.` + ) + .setFooter({ + text: `Timed out by ${interaction.user.tag}`, + iconURL: interaction.user.displayAvatarURL(), + }) + .setTimestamp(); + + await logChannel.send({ embeds: [logEmbed] }); + } + } catch (error) { + console.error("Error executing timeout command:", error); + + // If the interaction hasn't been replied to yet, reply with an error message + if (!replySent) { + 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/models/BannedUser.js b/models/BannedUser.js new file mode 100644 index 0000000..81c633c --- /dev/null +++ b/models/BannedUser.js @@ -0,0 +1,14 @@ +const mongoose = require("mongoose"); + +const bannedUserSchema = new mongoose.Schema({ + bannedUserId: { type: String, required: true }, + bannedUserTag: { type: String, required: true }, + bannerId: { type: String, required: true }, + bannerTag: { type: String, required: true }, + reason: { type: String, default: "No reason provided" }, + bannedAt: { type: Date, default: Date.now }, +}); + +const BannedUser = mongoose.model("BannedUser", bannedUserSchema); + +module.exports = BannedUser; diff --git a/models/KickedUser.js b/models/KickedUser.js new file mode 100644 index 0000000..cb07770 --- /dev/null +++ b/models/KickedUser.js @@ -0,0 +1,14 @@ +const mongoose = require("mongoose"); + +const kickedUserSchema = new mongoose.Schema({ + kickedUserId: { type: String, required: true }, + kickedUserTag: { type: String, required: true }, + kickerId: { type: String, required: true }, + kickerTag: { type: String, required: true }, + reason: { type: String, default: "No reason provided" }, + kickedAt: { type: Date, default: Date.now }, +}); + +const KickedUser = mongoose.model("KickedUser", kickedUserSchema); + +module.exports = KickedUser; diff --git a/models/TimedOutUser.js b/models/TimedOutUser.js new file mode 100644 index 0000000..22a00e7 --- /dev/null +++ b/models/TimedOutUser.js @@ -0,0 +1,15 @@ +const mongoose = require("mongoose"); + +const timedOutUserSchema = new mongoose.Schema({ + timedOutUserId: { type: String, required: true }, + timedOutUserTag: { type: String, required: true }, + moderatorId: { type: String, required: true }, + moderatorTag: { type: String, required: true }, + reason: { type: String, default: "No reason provided" }, + timeoutEnd: { type: Date, required: true }, + timedOutAt: { type: Date, default: Date.now }, +}); + +const TimedOutUser = mongoose.model("TimedOutUser", timedOutUserSchema); + +module.exports = TimedOutUser;