From f65ec8ca0f26b53771964ef29836bedc24f707b6 Mon Sep 17 00:00:00 2001 From: Ayden Jahola Date: Wed, 25 Sep 2024 23:07:02 +0100 Subject: [PATCH] bot: change moderation to be setup using discord permissions, add setup command to get rid of .env variables --- commands/fun/bored.js | 7 +- commands/moderation/ban.js | 16 +++-- commands/moderation/clearLeaderboard.js | 6 +- commands/moderation/kick.js | 11 ++- commands/moderation/purge.js | 6 +- commands/moderation/setup.js | 92 +++++++++++++++++++++++++ commands/moderation/timeout.js | 14 ++-- commands/moderation/userinfo.js | 16 +++-- commands/moderation/warn.js | 16 +++-- commands/utility/help.js | 12 +++- commands/verification/code.js | 37 +++++++--- commands/verification/verify.js | 40 ++++++++--- models/ServerSettings.js | 13 ++++ package.json | 3 +- 14 files changed, 230 insertions(+), 59 deletions(-) create mode 100644 commands/moderation/setup.js create mode 100644 models/ServerSettings.js diff --git a/commands/fun/bored.js b/commands/fun/bored.js index 952a052..840d8e4 100644 --- a/commands/fun/bored.js +++ b/commands/fun/bored.js @@ -36,9 +36,10 @@ module.exports = { await interaction.reply({ embeds: [embed] }); } catch (error) { console.error(error); - await interaction.reply( - "There was an error trying to fetch a random activity." - ); + await interaction.reply({ + content: "There was an error trying to fetch a random activity.", + epemeral: true, + }); } }, }; diff --git a/commands/moderation/ban.js b/commands/moderation/ban.js index 9830824..218ed3d 100644 --- a/commands/moderation/ban.js +++ b/commands/moderation/ban.js @@ -1,4 +1,8 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { + SlashCommandBuilder, + EmbedBuilder, + PermissionFlagsBits, +} = require("discord.js"); const BannedUser = require("../../models/BannedUser"); module.exports = { @@ -20,10 +24,10 @@ module.exports = { let replySent = false; try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Ban Members permission + if (!interaction.member.permissions.has(PermissionFlagsBits.BanMembers)) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); replySent = true; @@ -101,7 +105,7 @@ module.exports = { iconURL: interaction.user.displayAvatarURL(), }); - // Send confirmation as ephemeral message + // Send confirmation as an ephemeral message if (!replySent) { await interaction.reply({ embeds: [banEmbed], @@ -110,7 +114,7 @@ module.exports = { replySent = true; } - // log the ban in a designated channel + // Log the ban in a designated channel const logChannelId = process.env.LOG_CHANNEL_ID; const logChannel = interaction.guild.channels.cache.get(logChannelId); diff --git a/commands/moderation/clearLeaderboard.js b/commands/moderation/clearLeaderboard.js index 54980d4..7251f1c 100644 --- a/commands/moderation/clearLeaderboard.js +++ b/commands/moderation/clearLeaderboard.js @@ -9,10 +9,10 @@ module.exports = { async execute(interaction) { try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Manage Server permission + if (!interaction.member.permissions.has("ManageGuild")) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); return; diff --git a/commands/moderation/kick.js b/commands/moderation/kick.js index 458251b..7a5f0f2 100644 --- a/commands/moderation/kick.js +++ b/commands/moderation/kick.js @@ -21,10 +21,10 @@ module.exports = { async execute(interaction) { try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Kick Members permission + if (!interaction.member.permissions.has("KickMembers")) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); return; @@ -77,8 +77,7 @@ module.exports = { }); } 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 + // Handle DM errors appropriately, if needed } const kickEmbed = new EmbedBuilder() @@ -99,7 +98,7 @@ module.exports = { ephemeral: true, }); - // log the kick in a designated channel + // Log the kick in a designated channel const logChannelId = process.env.LOG_CHANNEL_ID; const logChannel = interaction.guild.channels.cache.get(logChannelId); diff --git a/commands/moderation/purge.js b/commands/moderation/purge.js index 1a05a2f..1b20467 100644 --- a/commands/moderation/purge.js +++ b/commands/moderation/purge.js @@ -26,10 +26,10 @@ module.exports = { async execute(interaction) { try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Manage Messages permission + if (!interaction.member.permissions.has("ManageMessages")) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); return; diff --git a/commands/moderation/setup.js b/commands/moderation/setup.js new file mode 100644 index 0000000..997d8d8 --- /dev/null +++ b/commands/moderation/setup.js @@ -0,0 +1,92 @@ +const { SlashCommandBuilder, PermissionFlagsBits } = require("discord.js"); +const ServerSettings = require("../../models/ServerSettings"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("setup") + .setDescription("Configure server settings for verification.") + .addChannelOption((option) => + option + .setName("logchannel") + .setDescription("Select the log channel for logging actions.") + .setRequired(true) + ) + .addChannelOption((option) => + option + .setName("generalchannel") + .setDescription("Select the general channel for join messages.") + .setRequired(true) + ) + .addChannelOption((option) => + option + .setName("verificationchannel") + .setDescription("Select the verification channel where users verify.") + .setRequired(true) + ) + .addRoleOption((option) => + option + .setName("verifiedrole") + .setDescription("Select the Verified role for verified users.") + .setRequired(true) + ) + .addStringOption((option) => + option + .setName("emaildomains") + .setDescription("Comma-separated list of allowed email domains.") + .setRequired(true) + ), + + async execute(interaction) { + // Check if the user has admin permissions + if ( + !interaction.member.permissions.has(PermissionFlagsBits.Administrator) + ) { + return interaction.reply({ + content: "You do not have permission to use this command.", + ephemeral: true, + }); + } + + const logChannel = interaction.options.getChannel("logchannel"); + const generalChannel = interaction.options.getChannel("generalchannel"); + const verificationChannel = interaction.options.getChannel( + "verificationchannel" + ); + const verifiedRole = interaction.options.getRole("verifiedrole"); + const emailDomains = interaction.options + .getString("emaildomains") + .split(","); + + try { + // Store the channel IDs instead of names + await ServerSettings.findOneAndUpdate( + { guildId: interaction.guild.id }, + { + guildId: interaction.guild.id, + logChannelId: logChannel.id, // Store log channel ID + verifiedRoleName: verifiedRole.name, + verificationChannelId: verificationChannel.id, // Store verification channel ID + generalChannelId: generalChannel.id, // Store general channel ID + emailDomains: emailDomains, + }, + { upsert: true, new: true } + ); + + interaction.reply({ + content: `Server settings have been updated successfully!\n + **Log Channel**: <#${logChannel.id}>\n + **General Channel**: <#${generalChannel.id}>\n + **Verification Channel**: <#${verificationChannel.id}>\n + **Verified Role**: ${verifiedRole.name}\n + **Allowed Email Domains**: ${emailDomains.join(", ")}`, + ephemeral: true, + }); + } catch (error) { + console.error("Error updating server settings:", error); + interaction.reply({ + content: "There was an error updating the server settings.", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/moderation/timeout.js b/commands/moderation/timeout.js index be83431..000465c 100644 --- a/commands/moderation/timeout.js +++ b/commands/moderation/timeout.js @@ -33,10 +33,14 @@ module.exports = { let replySent = false; try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Manage Roles permission + if ( + !interaction.member.permissions.has( + PermissionsBitField.Flags.ManageRoles + ) + ) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); replySent = true; @@ -98,6 +102,8 @@ module.exports = { color: "#ff0000", permissions: [], }); + + // Disable send messages permission for the timeout role in all channels interaction.guild.channels.cache.each(async (channel) => { await channel.permissionOverwrites.edit(timeoutRole, { SendMessages: false, @@ -153,7 +159,7 @@ module.exports = { replySent = true; } - // log the timeout in a designated channel + // Log the timeout in a designated channel const logChannelId = process.env.LOG_CHANNEL_ID; const logChannel = interaction.guild.channels.cache.get(logChannelId); diff --git a/commands/moderation/userinfo.js b/commands/moderation/userinfo.js index 94fcada..4e62681 100644 --- a/commands/moderation/userinfo.js +++ b/commands/moderation/userinfo.js @@ -1,4 +1,8 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { + SlashCommandBuilder, + EmbedBuilder, + PermissionsBitField, +} = require("discord.js"); module.exports = { data: new SlashCommandBuilder() @@ -14,10 +18,14 @@ module.exports = { async execute(interaction) { try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Manage Roles permission + if ( + !interaction.member.permissions.has( + PermissionsBitField.Flags.ManageRoles + ) + ) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); return; diff --git a/commands/moderation/warn.js b/commands/moderation/warn.js index 9449e7d..63b9140 100644 --- a/commands/moderation/warn.js +++ b/commands/moderation/warn.js @@ -1,4 +1,8 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { + SlashCommandBuilder, + EmbedBuilder, + PermissionsBitField, +} = require("discord.js"); const Warning = require("../../models/warning"); module.exports = { @@ -21,10 +25,14 @@ module.exports = { async execute(interaction) { try { - const requiredRoleId = process.env.MOD_ROLE_ID; - if (!interaction.member.roles.cache.has(requiredRoleId)) { + // Check if the user has the Manage Roles permission + if ( + !interaction.member.permissions.has( + PermissionsBitField.Flags.ManageRoles + ) + ) { await interaction.reply({ - content: "You do not have the required role to use this command!", + content: "You do not have permission to use this command!", ephemeral: true, }); return; diff --git a/commands/utility/help.js b/commands/utility/help.js index 89c2c61..b84c980 100644 --- a/commands/utility/help.js +++ b/commands/utility/help.js @@ -1,4 +1,8 @@ -const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const { + SlashCommandBuilder, + EmbedBuilder, + PermissionsBitField, +} = require("discord.js"); module.exports = { data: new SlashCommandBuilder() @@ -7,8 +11,10 @@ module.exports = { async execute(interaction, client) { try { - const modRoleId = process.env.MOD_ROLE_ID; - const isMod = interaction.member.roles.cache.has(modRoleId); + // Check if the user has the Manage Roles permission + const isMod = interaction.member.permissions.has( + PermissionsBitField.Flags.ManageRoles + ); const serverName = interaction.guild.name; diff --git a/commands/verification/code.js b/commands/verification/code.js index d79977e..dc96df9 100644 --- a/commands/verification/code.js +++ b/commands/verification/code.js @@ -1,5 +1,6 @@ const { SlashCommandBuilder } = require("discord.js"); const VerificationCode = require("../../models/VerificationCode"); +const ServerSettings = require("../../models/ServerSettings"); module.exports = { data: new SlashCommandBuilder() @@ -13,11 +14,24 @@ module.exports = { ), async execute(interaction, client) { - // Ensure command is only used in the specified verification channel - const verificationChannelName = process.env.VERIFICATION_CHANNEL_NAME; - if (interaction.channel.name !== verificationChannelName) { + // Fetch server settings from the database + const serverSettings = await ServerSettings.findOne({ + guildId: interaction.guild.id, + }); + + if (!serverSettings) { return interaction.reply({ - content: `This command can only be used in the #${verificationChannelName} channel.`, + content: + "Server settings have not been configured yet. Please contact an administrator.", + ephemeral: true, + }); + } + + // Ensure command is only used in the specified verification channel + const verificationChannelId = serverSettings.verificationChannelId; + if (interaction.channel.id !== verificationChannelId) { + return interaction.reply({ + content: `This command can only be used in <#${verificationChannelId}> channel.`, ephemeral: true, }); } @@ -33,6 +47,7 @@ module.exports = { } try { + // Find the verification code in the database const verificationEntry = await VerificationCode.findOne({ userId: interaction.user.id, code, @@ -45,7 +60,7 @@ module.exports = { }); } - const guild = client.guilds.cache.get(process.env.GUILD_ID); + const guild = client.guilds.cache.get(interaction.guild.id); if (!guild) { console.error("Guild not found."); @@ -66,13 +81,13 @@ module.exports = { } const role = guild.roles.cache.find( - (r) => r.name === process.env.VERIFIED_ROLE_NAME + (r) => r.name === serverSettings.verifiedRoleName ); if (!role) { - console.error(`Role "${process.env.VERIFIED_ROLE_NAME}" not found.`); + console.error(`Role "${serverSettings.verifiedRoleName}" not found.`); return interaction.reply({ - content: `The role "${process.env.VERIFIED_ROLE_NAME}" could not be found.`, + content: `The role "${serverSettings.verifiedRoleName}" could not be found.`, ephemeral: true, }); } @@ -89,9 +104,9 @@ module.exports = { // Delete the verification code entry await VerificationCode.deleteOne({ userId: interaction.user.id, code }); - // Get the admin log channel and send a log message + // Get the log channel and send a log message const adminLogChannel = client.channels.cache.get( - process.env.LOG_CHANNEL_ID + serverSettings.logChannelId ); if (adminLogChannel) { await adminLogChannel.send({ @@ -103,7 +118,7 @@ module.exports = { // Get the general channel and send a welcome message const generalChannel = client.channels.cache.get( - process.env.GENERAL_CHANNEL_ID + serverSettings.generalChannelId ); if (generalChannel) { await generalChannel.send({ diff --git a/commands/verification/verify.js b/commands/verification/verify.js index a9aa304..74947f4 100644 --- a/commands/verification/verify.js +++ b/commands/verification/verify.js @@ -1,11 +1,12 @@ const { SlashCommandBuilder } = require("discord.js"); const nodemailer = require("nodemailer"); const VerificationCode = require("../../models/VerificationCode"); +const ServerSettings = require("../../models/ServerSettings"); const transporter = nodemailer.createTransport({ service: "Gmail", auth: { - user: process.env.EMAIL_USER, + user: process.env.EMAIL_USER, // Email user and pass still from .env (for now) pass: process.env.EMAIL_PASS, }, }); @@ -22,27 +23,41 @@ module.exports = { ), async execute(interaction, client) { - // Ensure command is only used in the specified verification channel - const verificationChannelName = process.env.VERIFICATION_CHANNEL_NAME; - if (interaction.channel.name !== verificationChannelName) { + // Fetch the server settings from the database using guild ID + const serverSettings = await ServerSettings.findOne({ + guildId: interaction.guild.id, + }); + + if (!serverSettings) { return interaction.reply({ - content: `This command can only be used in the #${verificationChannelName} channel.`, + content: + "Server settings have not been configured yet. Please contact an administrator.", + ephemeral: true, + }); + } + + // Ensure command is only used in the specified verification channel + const verificationChannelId = serverSettings.verificationChannelId; + if (interaction.channel.id !== verificationChannelId) { + return interaction.reply({ + content: `This command can only be used in <#${verificationChannelId}> channel.`, ephemeral: true, }); } const email = interaction.options.getString("email"); const emailDomain = email.split("@")[1]; - const EMAIL_DOMAINS = process.env.EMAIL_DOMAINS.split(","); + const allowedEmailDomains = serverSettings.emailDomains; - if (!EMAIL_DOMAINS.includes(emailDomain)) { + // Check if the email domain is allowed + if (!allowedEmailDomains.includes(emailDomain)) { return interaction.reply({ content: "You must use a valid DCU email address.", ephemeral: true, }); } - const guild = client.guilds.cache.get(process.env.GUILD_ID); + const guild = client.guilds.cache.get(interaction.guild.id); if (!guild) { console.error("Guild not found."); @@ -63,13 +78,13 @@ module.exports = { } const role = guild.roles.cache.find( - (r) => r.name === process.env.VERIFIED_ROLE_NAME + (r) => r.name === serverSettings.verifiedRoleName ); if (!role) { - console.error(`Role "${process.env.VERIFIED_ROLE_NAME}" not found.`); + console.error(`Role "${serverSettings.verifiedRoleName}" not found.`); return interaction.reply({ - content: `Role "${process.env.VERIFIED_ROLE_NAME}" not found.`, + content: `Role "${serverSettings.verifiedRoleName}" not found.`, ephemeral: true, }); } @@ -81,6 +96,7 @@ module.exports = { }); } + // Generate a 6-digit verification code const verificationCode = Math.floor( 100000 + Math.random() * 900000 ).toString(); @@ -102,6 +118,7 @@ module.exports = { `; try { + // Send the verification email await transporter.sendMail({ from: `"${process.env.EMAIL_NAME}" <${process.env.EMAIL_USER}>`, to: email, @@ -109,6 +126,7 @@ module.exports = { html: emailHtml, }); + // Save the verification code and email in the database await VerificationCode.create({ userId: interaction.user.id, email: email, diff --git a/models/ServerSettings.js b/models/ServerSettings.js new file mode 100644 index 0000000..9601eac --- /dev/null +++ b/models/ServerSettings.js @@ -0,0 +1,13 @@ +const mongoose = require("mongoose"); + +const ServerSettingsSchema = new mongoose.Schema({ + guildId: { type: String, required: true, unique: true }, + logChannelId: { type: String, required: true }, + verifiedRoleName: { type: String, required: true }, + verificationChannelId: { type: String, required: true }, + generalChannelId: { type: String, required: true }, + emailDomains: { type: [String], required: true }, +}); + +const ServerSettings = mongoose.model("ServerSettings", ServerSettingsSchema); +module.exports = ServerSettings; diff --git a/package.json b/package.json index 1a23e9f..f475f8d 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "html-entities": "^2.5.2", "mongoose": "^8.6.0", "nodemailer": "^6.9.14", - "owoify-js": "^2.0.0" + "owoify-js": "^2.0.0", + "puppeteer": "^23.4.1" } }