diff --git a/.vscode/settings.json b/.vscode/settings.json index 05db7119..a114d9cd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "autoposter", + "bannable", "botstats", "crey", "davidzwa", @@ -11,7 +12,8 @@ "Ludoviko", "mkevenaar", "nicknelson", - "topgg" + "topgg", + "unban" ], "jest.jestCommandLine": "yarn run test:cov" } diff --git a/src/bot.js b/src/bot.js index d2e49c28..f66cafee 100644 --- a/src/bot.js +++ b/src/bot.js @@ -2,7 +2,7 @@ import { getEnvConfig } from './shared.js'; import { Client, Collection, Intents } from 'discord.js'; import mongoose from 'mongoose'; import { AutoPoster } from 'topgg-autoposter'; -import { resolveChannel, convertTime } from './tools/tools.js'; +import { resolveChannel, convertTime, swapPages } from './tools/tools.js'; import { Constants } from './constants.js'; import { readdirSync } from 'fs'; import { GuildService } from './database/guild.service.js'; @@ -34,6 +34,7 @@ export function createDiscordClient() { client.tools = { convertTime, resolveChannel, + swapPages, }; return client; diff --git a/src/commands/moderation/ban.js b/src/commands/moderation/ban.js new file mode 100644 index 00000000..1ab4d9c2 --- /dev/null +++ b/src/commands/moderation/ban.js @@ -0,0 +1,108 @@ +import { SlashCommandBuilder } from '@discordjs/builders'; +import { MessageEmbed, Permissions } from 'discord.js'; +import { botPermissions } from '../../tools/botPermissions.js'; +import { BotColors } from '../../constants.js'; + +export const permission = new botPermissions() + .setUserPerms(Permissions.FLAGS.BAN_MEMBERS) + .setUserMessage("You don't have permission to ban members!") + .setBotPerms(Permissions.FLAGS.BAN_MEMBERS) + .setBotMessage("It seems that I don't have permission ban members!"); + +export const data = new SlashCommandBuilder() + .setName('ban') + .setDescription('Bans a Member from a Guild') + .addUserOption((user) => { + return user.setName('user').setDescription('User that needs to be banned').setRequired(true); + }) + .addNumberOption((days) => { + return days + .setName('days') + .setDescription( + 'Number of days of messages to delete, must be between 0 and 7, inclusive; default == 2' + ) + .setRequired(false); + }) + .addStringOption((reason) => { + return reason.setName('reason').setDescription('The reason for the ban').setRequired(false); + }); + +export async function execute(interaction) { + let user = interaction.options.getUser('user'); + let days = interaction.options.getNumber('days'); + let reason = interaction.options.getString('reason'); + + let banMember = await interaction.guild.members.fetch(user.id); + + if (isNaN(days)) { + days = 2; + } + + if (Number(days) >= 7) days = 7; + if (Number(days) <= 0) days = 0; + + if (!reason) { + reason = `NO REASON PROVIDED`; + } + + const memberPosition = banMember.roles?.highest.rawPosition; + const moderationPosition = interaction.member.roles?.highest.rawPosition; + + if (moderationPosition <= memberPosition) { + let errorMessage = new MessageEmbed() + .setColor(BotColors.failed) + .setTitle('I cannot ban someone, who is above/equal to you'); + return interaction.reply({ embeds: [errorMessage], ephemeral: true }); + } + + if (!banMember.bannable) { + let errorMessage = new MessageEmbed() + .setColor(BotColors.failed) + .setTitle('The Member is not bannable, sorry!'); + return interaction.reply({ embeds: [errorMessage], ephemeral: true }); + } + + try { + if (!banMember.user.bot) { + let banMessage = new MessageEmbed() + .setColor(BotColors.default) + .setTitle(`You got banned by ${interaction.user.tag} from ${interaction.guild.name}`) + .setDescription(`Reason:\n> ${reason}`); + await banMember.user.send({ embeds: [banMessage] }).catch((error) => { + console.error(error); + let errorMessage = new MessageEmbed() + .setColor(BotColors.failed) + .setTitle(`Could not DM the Reason to: ${banMember.user.tag}`) + .setDescription(`${banMember.user}`); + return interaction.reply({ embeds: [errorMessage], ephemeral: true }); + }); + } + } catch (error) { + console.error(error); + let errorMessage = new MessageEmbed() + .setColor(BotColors.failed) + .setTitle(`Could not DM the Reason to: ${banMember.user.tag}`) + .setDescription(`${banMember.user}`); + return interaction.reply({ embeds: [errorMessage], ephemeral: true }); + } + + // Ban the member! + try { + banMember.ban({ days: days, reason: reason }).then(() => { + let banResult = new MessageEmbed() + .setColor(BotColors.default) + .setTitle(`Banned ${banMember.user.tag} (${banMember.user.id})`) + .setDescription(`Reason:\n> ${reason}`); + + return interaction.reply({ embeds: [banResult] }).catch((error) => { + console.error(error); + }); + }); + } catch (error) { + console.error(error); + await interaction.reply({ + content: `An issue has occurred while running the command. If this error keeps occurring please contact our development team.`, + ephemeral: true, + }); + } +} diff --git a/src/commands/moderation/listbans.js b/src/commands/moderation/listbans.js new file mode 100644 index 00000000..0e1c39d2 --- /dev/null +++ b/src/commands/moderation/listbans.js @@ -0,0 +1,27 @@ +import { SlashCommandBuilder } from '@discordjs/builders'; +import { Permissions } from 'discord.js'; +import { botPermissions } from '../../tools/botPermissions.js'; + +export const permission = new botPermissions() + .setUserPerms(Permissions.FLAGS.BAN_MEMBERS) + .setUserMessage("You don't have permission to ban members!") + .setBotPerms(Permissions.FLAGS.BAN_MEMBERS) + .setBotMessage("It seems that I don't have permission ban members!"); + +export const data = new SlashCommandBuilder() + .setName('listbans') + .setDescription('Shows all Bans of the Guild'); + +export async function execute(interaction, client) { + let allBans = await interaction.guild.bans + .fetch() + .then((bans) => + bans.map( + (ban) => + `**${ban.user.username}**#${ban.user.discriminator} (\`${ + ban.user.id + }\`)\n**Reason**:\n> ${ban.reason ? ban.reason : 'No Reason'}\n` + ) + ); + client.tools.swapPages(client, interaction, allBans, `All Bans of **${interaction.guild.name}**`); +} diff --git a/src/commands/moderation/unban.js b/src/commands/moderation/unban.js new file mode 100644 index 00000000..376fa36e --- /dev/null +++ b/src/commands/moderation/unban.js @@ -0,0 +1,49 @@ +import { SlashCommandBuilder } from '@discordjs/builders'; +import { MessageEmbed, Permissions } from 'discord.js'; +import { botPermissions } from '../../tools/botPermissions.js'; +import { BotColors } from '../../constants.js'; + +export const permission = new botPermissions() + .setUserPerms(Permissions.FLAGS.BAN_MEMBERS) + .setUserMessage("You don't have permission to ban members!") + .setBotPerms(Permissions.FLAGS.BAN_MEMBERS) + .setBotMessage("It seems that I don't have permission ban members!"); + +export const data = new SlashCommandBuilder() + .setName('unban') + .setDescription('Removes a ban from a Member in this Guild') + .addUserOption((user) => { + return user.setName('user').setDescription('User that needs to be unbanned').setRequired(true); + }); + +export async function execute(interaction) { + let user = interaction.options.getUser('user'); + + let bans = await interaction.guild.bans.fetch().catch(() => {}); + if (!bans.map((b) => b?.user.id).includes(user.id)) { + let errorMessage = new MessageEmbed() + .setColor(BotColors.failed) + .setTitle('The User with that Id is not banned in this Server!'); + return interaction.reply({ embeds: [errorMessage], ephemeral: true }); + } + + try { + let banUser = bans.map((b) => b?.user).find((u) => u.id == user.id); + interaction.guild.members.unban(banUser ? banUser.id : user.id); + return interaction.reply({ + embeds: [ + new MessageEmbed() + .setColor(BotColors.default) + .setTitle(`Successfully Unbanned ${banUser.username}#${banUser.discriminator}`) + .setDescription(`Use: /listbans to see all ${bans.size - 1} Bans!`), + ], + }); + } catch (error) { + console.error(error); + let errorMessage = new MessageEmbed() + .setColor(BotColors.failed) + .setTitle(`Could unban: ${banMember.user.tag}`) + .setDescription(`${banMember.user}`); + return interaction.reply({ embeds: [errorMessage], ephemeral: true }); + } +} diff --git a/src/tools/tools.js b/src/tools/tools.js index 50270cc7..d66693c4 100644 --- a/src/tools/tools.js +++ b/src/tools/tools.js @@ -1,3 +1,6 @@ +import { MessageEmbed, MessageButton, MessageActionRow } from 'discord.js'; +import { BotColors } from '../constants.js'; + export async function resolveChannel(search, guild) { let channel = null; if (!search || typeof search !== 'string') return; @@ -47,3 +50,191 @@ export async function convertTime(milliseconds) { .replace('{secs}', secs); return sentence; } + +export async function swapPages(client, message, description, TITLE) { + //has the interaction already been deferred? If not, defer the reply. + if (message.deferred == false) { + await message.deferReply(); + } + + let swapUser = message.member; + + let currentPage = 0; + //GET ALL EMBEDS + let embeds = []; + //if input is an array + if (Array.isArray(description)) { + try { + let k = 20; + for (let i = 0; i < description.length; i += 20) { + const current = description.slice(i, k); + k += 20; + const embed = new MessageEmbed() + .setDescription(current.join('\n')) + .setTitle(TITLE) + .setColor(BotColors.default); + embeds.push(embed); + } + } catch (error) { + console.error(error); + } + } else { + try { + let k = 1000; + for (let i = 0; i < description.length; i += 1000) { + const current = description.slice(i, k); + k += 1000; + const embed = new MessageEmbed() + .setDescription(current) + .setTitle(TITLE) + .setColor(BotColors.default); + embeds.push(embed); + } + } catch (error) { + console.error(error); + } + } + if (embeds.length === 0) { + let embed = new MessageEmbed().setTitle(`No Content`).setColor(BotColors.default); + return message.reply({ embeds: [embed] }); + } + if (embeds.length === 1) return message.reply({ embeds: [embeds[0]] }); + + let button_back = new MessageButton() + .setStyle('SUCCESS') + .setCustomId('1') + .setEmoji('⬅️') + .setLabel('Back'); + let button_home = new MessageButton() + .setStyle('DANGER') + .setCustomId('2') + .setEmoji('🏠') + .setLabel('Home'); + let button_forward = new MessageButton() + .setStyle('SUCCESS') + .setCustomId('3') + .setEmoji('➡️') + .setLabel('Forward'); + let button_stop = new MessageButton() + .setStyle('DANGER') + .setCustomId('stop') + .setEmoji('🛑') + .setLabel('Stop'); + const allButtons = [ + new MessageActionRow().addComponents([button_back, button_home, button_forward, button_stop]), + ]; + //Send message with buttons + let swapMsg = await message.reply({ + content: `***Click on the __Buttons__ to swap the Pages***`, + embeds: [embeds[0]], + components: allButtons, + fetchReply: true, + ephemeral: true, + }); + //create a collector for the thingy + const collector = swapMsg.createMessageComponentCollector({ + filter: (i) => + i?.isButton() && + i?.user && + i?.user.id == swapUser.id && + i?.message.member.id == client.user.id, + time: 180e3, + }); //collector for 5 seconds + //array of all embeds, here simplified just 10 embeds with numbers 0 - 9 + collector.on('collect', async (b) => { + if (b?.user.id !== message.member.id) + return b?.reply({ + content: `**Only the one who used this command is allowed to react!**`, + ephemeral: true, + }); + //page forward + if (b?.customId == '1') { + collector.resetTimer(); + //b?.reply("***Swapping a PAGE FORWARD***, *please wait 2 Seconds for the next Input*", true) + if (currentPage !== 0) { + currentPage -= 1; + await swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents[swapMsg.components], + }) + .catch(() => {}); + await b?.deferUpdate(); + } else { + currentPage = embeds.length - 1; + await swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents[swapMsg.components], + }) + .catch(() => {}); + await b?.deferUpdate(); + } + } + //go home + else if (b?.customId == '2') { + collector.resetTimer(); + //b?.reply("***Going Back home***, *please wait 2 Seconds for the next Input*", true) + currentPage = 0; + await swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents[swapMsg.components], + }) + .catch(() => {}); + await b?.deferUpdate(); + } + //go forward + else if (b?.customId == '3') { + collector.resetTimer(); + //b?.reply("***Swapping a PAGE BACK***, *please wait 2 Seconds for the next Input*", true) + if (currentPage < embeds.length - 1) { + currentPage++; + await swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents[swapMsg.components], + }) + .catch(() => {}); + await b?.deferUpdate(); + } else { + currentPage = 0; + await swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents[swapMsg.components], + }) + .catch(() => {}); + await b?.deferUpdate(); + } + } + //go forward + else if (b?.customId == 'stop') { + await swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents(swapMsg.components), + }) + .catch(() => {}); + await b?.deferUpdate(); + collector.stop('stopped'); + } + }); + collector.on('end', (reason) => { + if (reason != 'stopped') { + swapMsg + .edit({ + embeds: [embeds[currentPage]], + components: getDisabledComponents(swapMsg.components), + }) + .catch(() => {}); + } + }); +} + +function getDisabledComponents(MessageComponents) { + if (!MessageComponents) return []; // Returning so it doesn't crash + return MessageComponents.map(({ components }) => { + return new MessageActionRow().addComponents(components.map((c) => c.setDisabled(true))); + }); +}