From 6de4da59ed1f7ad54150b453a67c94eb155c0254 Mon Sep 17 00:00:00 2001 From: pineafan Date: Tue, 7 Mar 2023 20:43:44 +0000 Subject: [PATCH] Please look over - not for production --- src/commands/mod/ban.ts | 48 ++++++++++--------- src/commands/mod/kick.ts | 29 ++++++++---- src/commands/mod/mute.ts | 42 ++++++++++------- src/commands/mod/nick.ts | 77 ++++++++++++++++++++---------- src/commands/mod/warn.ts | 42 ++++++++++------- src/commands/nucleus/stats.ts | 5 +- src/commands/settings/automod.ts | 7 ++- src/config/emojis.json | 8 ++++ src/events/guildMemberUpdate.ts | 1 - src/events/guildUpdate.ts | 2 +- src/events/interactionCreate.ts | 48 ++++++++++++++++++- src/events/memberJoin.ts | 6 ++- src/events/messageCreate.ts | 2 +- src/reflex/scanners.ts | 81 +++++++++++++++++++++++++++++++- src/reflex/statsChannelUpdate.ts | 15 +++--- src/reflex/welcome.ts | 3 +- src/utils/confirmationMessage.ts | 77 ++++++++++++++++++++++++++---- src/utils/database.ts | 27 ++++++----- 18 files changed, 384 insertions(+), 136 deletions(-) diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban.ts index 628b607..88f7b39 100644 --- a/src/commands/mod/ban.ts +++ b/src/commands/mod/ban.ts @@ -3,9 +3,9 @@ import Discord, { GuildMember, ActionRowBuilder, ButtonBuilder, - User, ButtonStyle, - SlashCommandSubcommandBuilder + SlashCommandSubcommandBuilder, + ButtonInteraction } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; @@ -29,8 +29,16 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setRequired(false) ); -const callback = async (interaction: CommandInteraction): Promise => { +const callback = async (interaction: CommandInteraction | ButtonInteraction, member?: GuildMember): Promise => { if (!interaction.guild) return; + let deleteDays; + if (!interaction.isButton()) { + member = interaction.options.getMember("user") as GuildMember; + deleteDays = (interaction.options.get("delete")?.value as number | null) ?? 0 + } else { + deleteDays = 0; + } + if (!member) return; const { renderUser } = client.logger; // TODO:[Modals] Replace the command arguments with a modal let reason = null; @@ -44,15 +52,12 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle("Ban") .setDescription( keyValueList({ - user: renderUser(interaction.options.getUser("user")!), + user: renderUser(member.user), reason: reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*" }) + `The user **will${notify ? "" : " not"}** be notified\n` + - `${addPlurals( - (interaction.options.get("delete")?.value as number | null) ?? 0, - "day" - )} of messages will be deleted\n\n` + - `Are you sure you want to ban <@!${(interaction.options.getMember("user") as GuildMember).id}>?` + `${addPlurals(deleteDays, "day")} of messages will be deleted\n\n` + + `Are you sure you want to ban <@!${member.id}>?` ) .addCustomBoolean( "notify", @@ -113,23 +118,21 @@ const callback = async (interaction: CommandInteraction): Promise => { .setURL( config.moderation.ban.link.replaceAll( "{id}", - (interaction.options.getMember("user") as GuildMember).id + member.id ) ) ) ); } - dmMessage = await (interaction.options.getMember("user") as GuildMember).send(messageData); + dmMessage = await member.send(messageData); dmSent = true; } } catch { dmSent = false; } try { - const member = interaction.options.getMember("user") as GuildMember; - const days: number = (interaction.options.get("delete")?.value as number | null) ?? 0; member.ban({ - deleteMessageSeconds: days * 24 * 60 * 60, + deleteMessageSeconds: deleteDays * 24 * 60 * 60, reason: reason ?? "*No reason provided*" }); await client.database.history.create("ban", interaction.guild.id, member.user, interaction.user, reason); @@ -189,23 +192,22 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = async (interaction: CommandInteraction, partial: boolean = false) => { +const check = (interaction: CommandInteraction | ButtonInteraction, partial: boolean = false, target?: GuildMember) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; // Check if the user has ban_members permission if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission"; if (partial) return true; const me = interaction.guild.members.me!; - let apply = interaction.options.getUser("user") as User | GuildMember; + let apply: GuildMember; + if (interaction.isButton()) { + apply = target!; + } else { + apply = interaction.options.getMember("user") as GuildMember; + }; const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0; - let applyPos = 0; - try { - apply = (await interaction.guild.members.fetch(apply.id)) as GuildMember; - applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0; - } catch { - apply = apply as User; - } + const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0; // Do not allow banning the owner if (member.id === interaction.guild.ownerId) return "You cannot ban the owner of the server"; // Check if Nucleus can ban the member diff --git a/src/commands/mod/kick.ts b/src/commands/mod/kick.ts index c4f1867..4d325a2 100644 --- a/src/commands/mod/kick.ts +++ b/src/commands/mod/kick.ts @@ -5,7 +5,8 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, - SlashCommandSubcommandBuilder + SlashCommandSubcommandBuilder, + ButtonInteraction } from "discord.js"; // @ts-expect-error import humanizeDuration from "humanize-duration"; @@ -22,8 +23,12 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Kicks a user from the server") .addUserOption((option) => option.setName("user").setDescription("The user to kick").setRequired(true)); -const callback = async (interaction: CommandInteraction): Promise => { +const callback = async (interaction: CommandInteraction | ButtonInteraction, member?: GuildMember): Promise => { if (!interaction.guild) return; + if (!interaction.isButton()) { + member = interaction.options.getMember("user") as GuildMember; + } + if (!member) return; const { renderUser } = client.logger; // TODO:[Modals] Replace this with a modal let reason: string | null = null; @@ -37,9 +42,9 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle("Kick") .setDescription( keyValueList({ - user: renderUser(interaction.options.getUser("user")!), + user: renderUser(member.user), reason: reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*" - }) + `Are you sure you want to kick <@!${(interaction.options.getMember("user") as GuildMember).id}>?` + }) + `Are you sure you want to kick <@!${member.id}>?` ) .setColor("Danger") .addCustomBoolean( @@ -101,21 +106,20 @@ const callback = async (interaction: CommandInteraction): Promise => { .setURL( config.moderation.kick.link.replaceAll( "{id}", - (interaction.options.getMember("user") as GuildMember).id + member.id ) ) ) ); } - dmMessage = await (interaction.options.getMember("user") as GuildMember).send(messageData); + dmMessage = await member.send(messageData); dmSent = true; } } catch { dmSent = false; } try { - (interaction.options.getMember("user") as GuildMember).kick(reason || "No reason provided"); - const member = interaction.options.getMember("user") as GuildMember; + member.kick(reason || "No reason provided"); await client.database.history.create("kick", interaction.guild.id, member.user, interaction.user, reason); const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger; const timeInServer = member.joinedTimestamp @@ -186,7 +190,7 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = (interaction: CommandInteraction, partial: boolean = false) => { +const check = (interaction: CommandInteraction | ButtonInteraction, partial: boolean = false, target?: GuildMember) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; @@ -195,7 +199,12 @@ const check = (interaction: CommandInteraction, partial: boolean = false) => { if (partial) return true; const me = interaction.guild.members.me!; - const apply = interaction.options.getMember("user") as GuildMember; + let apply: GuildMember; + if (interaction.isButton()) { + apply = target!; + } else { + apply = interaction.options.getMember("user") as GuildMember; + } const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0; const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0; diff --git a/src/commands/mod/mute.ts b/src/commands/mod/mute.ts index 39c9e5e..51ffc4d 100644 --- a/src/commands/mod/mute.ts +++ b/src/commands/mod/mute.ts @@ -1,5 +1,5 @@ import { LinkWarningFooter, LoadingEmbed } from "../../utils/defaults.js"; -import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; @@ -49,16 +49,22 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setRequired(false) ); -const callback = async (interaction: CommandInteraction): Promise => { +const callback = async (interaction: CommandInteraction | ButtonInteraction, member?: GuildMember): Promise => { if (!interaction.guild) return; const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger; - const member = interaction.options.getMember("user") as GuildMember; - const time: { days: number; hours: number; minutes: number; seconds: number } = { - days: (interaction.options.get("days")?.value as number | null) ?? 0, - hours: (interaction.options.get("hours")?.value as number | null) ?? 0, - minutes: (interaction.options.get("minutes")?.value as number | null) ?? 0, - seconds: (interaction.options.get("seconds")?.value as number | null) ?? 0 - }; + let time: { days: number; hours: number; minutes: number; seconds: number } | null = null; + if (!interaction.isButton()) { + member = interaction.options.getMember("user") as GuildMember; + time = { + days: (interaction.options.get("days")?.value as number | null) ?? 0, + hours: (interaction.options.get("hours")?.value as number | null) ?? 0, + minutes: (interaction.options.get("minutes")?.value as number | null) ?? 0, + seconds: (interaction.options.get("seconds")?.value as number | null) ?? 0 + }; + } else { + time = {days: 0, hours: 0, minutes: 0, seconds: 0}; + } + if (!member) return; const config = await client.database.guilds.read(interaction.guild.id); let serverSettingsDescription = config.moderation.mute.timeout ? "given a timeout" : ""; if (config.moderation.mute.role) @@ -198,7 +204,7 @@ const callback = async (interaction: CommandInteraction): Promise => { "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), async () => - await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, reason), + await create(interaction.guild!, member!.user, interaction.user, reason), "An appeal ticket will be created when Confirm is clicked", null, "CONTROL.TICKET", @@ -275,12 +281,7 @@ const callback = async (interaction: CommandInteraction): Promise => { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(config.moderation.mute.text) - .setURL( - config.moderation.mute.link.replaceAll( - "{id}", - (interaction.options.getMember("user") as GuildMember).id - ) - ) + .setURL(config.moderation.mute.link.replaceAll("{id}", member.id)) ) ); } @@ -399,14 +400,19 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = async (interaction: CommandInteraction, partial: boolean = false) => { +const check = (interaction: CommandInteraction | ButtonInteraction, partial: boolean = false, target?: GuildMember) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; // Check if the user has moderate_members permission if (!member.permissions.has("ModerateMembers")) return "You do not have the *Moderate Members* permission"; if (partial) return true; const me = interaction.guild.members.me!; - const apply = interaction.options.getMember("user") as GuildMember; + let apply; + if (interaction.isButton()) { + apply = target!; + } else { + apply = interaction.options.getMember("user") as GuildMember; + } const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0; const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0; const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0; diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts index 8b33551..743579b 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -1,5 +1,5 @@ import { LinkWarningFooter } from "./../../utils/defaults.js"; -import { ActionRowBuilder, ButtonBuilder, CommandInteraction, GuildMember, ButtonStyle, Message } from "discord.js"; +import { ActionRowBuilder, ButtonBuilder, CommandInteraction, GuildMember, ButtonStyle, Message, ButtonInteraction, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; @@ -17,31 +17,37 @@ const command = (builder: SlashCommandSubcommandBuilder) => option.setName("name").setDescription("The name to set | Leave blank to clear").setRequired(false) ); -const callback = async (interaction: CommandInteraction): Promise => { +const callback = async (interaction: CommandInteraction | ButtonInteraction, member?: GuildMember): Promise => { const { log, NucleusColors, entry, renderDelta, renderUser } = client.logger; + let newNickname; + if (!interaction.isButton()) { + member = interaction.options.getMember("user") as GuildMember; + newNickname = interaction.options.get("name")?.value as string | undefined; + } + if (!member) return; // TODO:[Modals] Replace this with a modal - let notify = true; + let notify = false; let confirmation; let timedOut = false; let success = false; let createAppealTicket = false; - let firstRun = true; + let firstRun = !interaction.isButton(); do { confirmation = await new confirmationMessage(interaction) .setEmoji("PUNISH.NICKNAME.RED") .setTitle("Nickname") .setDescription( keyValueList({ - user: renderUser(interaction.options.getUser("user")!), + user: renderUser(member.user), "new nickname": `${ - (interaction.options.get("name")?.value as string) - ? (interaction.options.get("name")?.value as string) + newNickname + ? newNickname : "*No nickname*" }` }) + `Are you sure you want to ${ - (interaction.options.get("name")?.value as string) ? "change" : "clear" - } <@!${(interaction.options.getMember("user") as GuildMember).id}>'s nickname?` + newNickname ? "change" : "clear" + } <@!${member.id}>'s nickname?` ) .setColor("Danger") .addCustomBoolean( @@ -51,7 +57,7 @@ const callback = async (interaction: CommandInteraction): Promise => { async () => await create( interaction.guild!, - interaction.options.getUser("user")!, + member!.user, interaction.user, "Nickname changed" ), @@ -70,6 +76,25 @@ const callback = async (interaction: CommandInteraction): Promise => { "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) + .addModal( + "Change nickname", + "ICONS.EDIT", + "modal", + newNickname ?? "", + new ModalBuilder() + .setTitle("Editing nickname") + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId("default") + .setLabel("Nickname") + .setMaxLength(32) + .setRequired(false) + .setStyle(TextInputStyle.Short) + .setValue(newNickname ? newNickname : " ") + ) + ) + ) .setFailedMessage("No changes were made", "Success", "PUNISH.NICKNAME.GREEN") .send(!firstRun); firstRun = false; @@ -79,6 +104,7 @@ const callback = async (interaction: CommandInteraction): Promise => { notify = confirmation.components["notify"]!.active; createAppealTicket = confirmation.components["appeal"]!.active; } + if (confirmation.modals) newNickname = confirmation.modals![0]!.value } while (!timedOut && !success); if (timedOut || !success) return; let dmSent = false; @@ -96,10 +122,10 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle("Nickname changed") .setDescription( `Your nickname was ${ - (interaction.options.get("name")?.value as string) ? "changed" : "cleared" + newNickname ? "changed" : "cleared" } in ${interaction.guild!.name}.` + - ((interaction.options.get("name")?.value as string) - ? `\nIt is now: ${interaction.options.get("name")?.value as string}` + (newNickname + ? `\nIt is now: ${newNickname}` : "") + "\n\n" + (createAppealTicket @@ -122,26 +148,22 @@ const callback = async (interaction: CommandInteraction): Promise => { .setURL( config.moderation.nick.link.replaceAll( "{id}", - (interaction.options.getMember("user") as GuildMember).id + member.id ) ) ) ); } - dmMessage = await (interaction.options.getMember("user") as GuildMember).send(messageData); + dmMessage = await member.send(messageData); dmSent = true; } } catch { dmSent = false; } - let member: GuildMember; let before: string | null; - let nickname: string | undefined; try { - member = interaction.options.getMember("user") as GuildMember; before = member.nickname; - nickname = interaction.options.get("name")?.value as string | undefined; - member.setNickname(nickname ?? null, "Nucleus Nickname command"); + member.setNickname(newNickname ?? null, "Nucleus Nickname command"); await client.database.history.create( "nickname", interaction.guild!.id, @@ -149,7 +171,7 @@ const callback = async (interaction: CommandInteraction): Promise => { interaction.user, null, before, - nickname + newNickname ); } catch { await interaction.editReply({ @@ -175,9 +197,9 @@ const callback = async (interaction: CommandInteraction): Promise => { timestamp: Date.now() }, list: { - memberId: entry(member.id, `\`${member.id}\``), + member: entry(member.id, renderUser(member.user)), before: entry(before, before ?? "*No nickname set*"), - after: entry(nickname ?? null, nickname ?? "*No nickname set*"), + after: entry(newNickname ?? null, newNickname ?? "*No nickname set*"), updated: entry(Date.now(), renderDelta(Date.now())), updatedBy: entry(interaction.user.id, renderUser(interaction.user)) }, @@ -210,13 +232,18 @@ const callback = async (interaction: CommandInteraction): Promise => { }); }; -const check = async (interaction: CommandInteraction, partial: boolean = false) => { +const check = async (interaction: CommandInteraction | ButtonInteraction, partial: boolean, target?: GuildMember) => { const member = interaction.member as GuildMember; // Check if the user has manage_nicknames permission if (!member.permissions.has("ManageNicknames")) return "You do not have the *Manage Nicknames* permission"; if (partial) return true; const me = interaction.guild!.members.me!; - const apply = interaction.options.getMember("user") as GuildMember; + let apply: GuildMember; + if (interaction.isButton()) { + apply = target!; + } else { + apply = interaction.options.getMember("user") as GuildMember; + } const memberPos = member.roles.cache.size ? member.roles.highest.position : 0; const mePos = me.roles.cache.size ? me.roles.highest.position : 0; const applyPos = apply.roles.cache.size ? apply.roles.highest.position : 0; diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts index 5d1bd94..8408303 100644 --- a/src/commands/mod/warn.ts +++ b/src/commands/mod/warn.ts @@ -1,4 +1,4 @@ -import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; @@ -14,9 +14,11 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Warns a user") .addUserOption((option) => option.setName("user").setDescription("The user to warn").setRequired(true)); -const callback = async (interaction: CommandInteraction): Promise => { - if (interaction.guild === null) return; +const callback = async (interaction: CommandInteraction | ButtonInteraction, member?: GuildMember): Promise => { + if (!interaction.guild) return; const { log, NucleusColors, renderUser, entry } = client.logger; + if (!interaction.isButton()) member = interaction.options.getMember("user") as GuildMember; + if (!member) return; // TODO:[Modals] Replace this with a modal let reason: string | null = null; let notify = true; @@ -30,9 +32,9 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle("Warn") .setDescription( keyValueList({ - user: renderUser(interaction.options.getUser("user")!), + user: renderUser(member.user), reason: reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*" - }) + `Are you sure you want to warn <@!${(interaction.options.getMember("user") as GuildMember).id}>?` + }) + `Are you sure you want to warn <@!${member.id}>?` ) .setColor("Danger") .addCustomBoolean( @@ -40,7 +42,7 @@ const callback = async (interaction: CommandInteraction): Promise => { "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), async () => - await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, reason), + await create(interaction.guild!, member!.user, interaction.user, reason), "An appeal ticket will be created", null, "CONTROL.TICKET", @@ -111,13 +113,13 @@ const callback = async (interaction: CommandInteraction): Promise => { .setURL( config.moderation.warn.link.replaceAll( "{id}", - (interaction.options.getMember("user") as GuildMember).id + member.id ) ) ) ); } - await (interaction.options.getMember("user") as GuildMember).send(messageData); + await member.send(messageData); dmSent = true; } } catch (e) { @@ -134,8 +136,8 @@ const callback = async (interaction: CommandInteraction): Promise => { }, list: { user: entry( - (interaction.options.getMember("user") as GuildMember).user.id, - renderUser((interaction.options.getMember("user") as GuildMember).user) + member.user.id, + renderUser(member.user) ), warnedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)), reason: reason ? reason : "*No reason provided*" @@ -152,7 +154,7 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.history.create( "warn", interaction.guild.id, - (interaction.options.getMember("user") as GuildMember).user, + member.user, interaction.user, reason ); @@ -177,7 +179,7 @@ const callback = async (interaction: CommandInteraction): Promise => { components: [] }); } else { - const canSeeChannel = (interaction.options.getMember("user") as GuildMember) + const canSeeChannel = member .permissionsIn(interaction.channel as Discord.TextChannel) .has("ViewChannel"); const m = (await interaction.editReply({ @@ -235,9 +237,9 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription("You have been warned" + (reason ? ` for:\n> ${reason}` : ".")) .setStatus("Danger") ], - content: `<@!${(interaction.options.getMember("user") as GuildMember).id}>`, + content: `<@!${member.id}>`, allowedMentions: { - users: [(interaction.options.getMember("user") as GuildMember).id] + users: [member.id] } }); return await interaction.editReply({ @@ -271,7 +273,7 @@ const callback = async (interaction: CommandInteraction): Promise => { } else if (component.customId === "ticket") { const ticketChannel = await create( interaction.guild, - interaction.options.getUser("user")!, + member.user, interaction.user, reason, "Warn Notification" @@ -302,13 +304,17 @@ const callback = async (interaction: CommandInteraction): Promise => { } }; -const check = (interaction: CommandInteraction, partial: boolean = false) => { +const check = (interaction: CommandInteraction | ButtonInteraction, partial: boolean = false, target?: GuildMember) => { if (!interaction.guild) return; const member = interaction.member as GuildMember; if (!member.permissions.has("ModerateMembers")) return "You do not have the *Moderate Members* permission"; if (partial) return true; - const apply = interaction.options.getMember("user") as GuildMember | null; - if (apply === null) return "That member is not in the server"; + let apply: GuildMember; + if (interaction.isButton()) { + apply = target!; + } else { + apply = interaction.options.getMember("user") as GuildMember; + } const memberPos = member.roles.cache.size ? member.roles.highest.position : 0; const applyPos = apply.roles.cache.size ? apply.roles.highest.position : 0; // Do not allow warning bots diff --git a/src/commands/nucleus/stats.ts b/src/commands/nucleus/stats.ts index 058695c..1eceae2 100644 --- a/src/commands/nucleus/stats.ts +++ b/src/commands/nucleus/stats.ts @@ -28,7 +28,10 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription(description) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ], components: [new ActionRowBuilder().addComponents(new ButtonBuilder().setCustomId("admin").setLabel("Admin Panel").setStyle(ButtonStyle.Primary))] + ], components: [new ActionRowBuilder().addComponents( + new ButtonBuilder().setCustomId("admin").setLabel("Admin Panel").setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId("mod:nickname:599498449733550102").setLabel("Testing").setStyle(ButtonStyle.Primary) + )] }); const modal = new ModalBuilder() diff --git a/src/commands/settings/automod.ts b/src/commands/settings/automod.ts index 9d59520..db4a1c4 100644 --- a/src/commands/settings/automod.ts +++ b/src/commands/settings/automod.ts @@ -1069,17 +1069,19 @@ const callback = async (interaction: CommandInteraction): Promise => { closed = true; continue; } - await i.deferUpdate(); if (i.isButton()) { + await i.deferUpdate(); await client.database.guilds.write(interaction.guild.id, { filters: config }); await client.memory.forceUpdate(interaction.guild.id); } else { switch (i.values[0]) { case "invites": { + i.deferUpdate(); config.invite = await inviteMenu(i, m, config.invite); break; } case "mentions": { + i.deferUpdate(); config.pings = await mentionMenu(i, m, config.pings); break; } @@ -1088,15 +1090,18 @@ const callback = async (interaction: CommandInteraction): Promise => { break; } case "malware": { + i.deferUpdate(); config.malware = !config.malware; break; } case "images": { + i.deferUpdate(); const next = await imageMenu(i, m, config.images); config.images = next; break; } case "clean": { + i.deferUpdate(); const next = await cleanMenu(i, m, config.clean); config.clean = next; break; diff --git a/src/config/emojis.json b/src/config/emojis.json index d1398cb..585934d 100644 --- a/src/config/emojis.json +++ b/src/config/emojis.json @@ -49,6 +49,14 @@ "RULES": "990213153080115250", "FORUM": "1061706437526552716", "CATEGORY": "1064943289708597348" + }, + "FLAGS": { + "RED": "1082719687219101800", + "YELLOW": "1082719684060794890", + "GREEN": "1082719681326108763", + "BLUE": "1082719679161843734", + "PURPLE": "1082719686292156628", + "GRAY": "1082719682492125337" } }, "CONTROL": { diff --git a/src/events/guildMemberUpdate.ts b/src/events/guildMemberUpdate.ts index a0acd34..41a22c1 100644 --- a/src/events/guildMemberUpdate.ts +++ b/src/events/guildMemberUpdate.ts @@ -111,7 +111,6 @@ export async function callback(client: NucleusClient, before: GuildMember, after timestamp: Date.now() }, list: { - memberId: entry(after.id, `\`${after.id}\``), name: entry(after.user.id, renderUser(after.user)), before: entry(before.nickname, before.nickname ? before.nickname : "*None*"), after: entry(after.nickname, after.nickname ? after.nickname : "*None*"), diff --git a/src/events/guildUpdate.ts b/src/events/guildUpdate.ts index dbd747f..c48c82f 100644 --- a/src/events/guildUpdate.ts +++ b/src/events/guildUpdate.ts @@ -5,7 +5,7 @@ import { callback as statsChannelUpdate } from "../reflex/statsChannelUpdate.js" export const event = "guildUpdate"; export async function callback(client: NucleusClient, before: Guild, after: Guild) { - await statsChannelUpdate(client, after.members.me!); + await statsChannelUpdate(after.members.me!.user, after); const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger; if (!(await isLogging(after.id, "guildUpdate"))) return; const auditLog = (await getAuditLog(after, AuditLogEvent.GuildUpdate)).filter( diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index c91f8cd..4a8d941 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -4,13 +4,30 @@ import create from "../actions/tickets/create.js"; import close from "../actions/tickets/delete.js"; import createTranscript from "../premium/createTranscript.js"; -import type { Interaction } from "discord.js"; +import type { ButtonInteraction, Interaction } from "discord.js"; import type Discord from "discord.js"; import type { NucleusClient } from "../utils/client.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; +import { callback as banCallback, check as banCheck } from "../commands/mod/ban.js"; +import { callback as kickCallback, check as kickCheck } from "../commands/mod/kick.js"; +import { callback as muteCallback, check as muteCheck } from "../commands/mod/mute.js"; +import { callback as nicknameCallback, check as nicknameCheck } from "../commands/mod/nick.js"; +import { callback as warnCallback, check as warnCheck } from "../commands/mod/warn.js"; + export const event = "interactionCreate"; +async function errorMessage(interaction: ButtonInteraction, message: string) { + await interaction.reply({ + embeds: [new EmojiEmbed() + .setDescription(message) + .setStatus("Danger") + ], + ephemeral: true, + components: [] + }); +} + async function interactionCreate(interaction: Interaction) { if (interaction.isButton()) { switch (interaction.customId) { @@ -36,6 +53,35 @@ async function interactionCreate(interaction: Interaction) { return await modifySuggestion(interaction, false); } } + // Mod actions + if (interaction.customId.startsWith("mod:")) { + const action = interaction.customId.split(":")[1]; + const memberId = interaction.customId.split(":")[2]; + const member = await interaction.guild?.members.fetch(memberId!); + switch (action) { + case "kick": { + const check = await kickCheck(interaction, false, member) + if (check !== true) return await errorMessage(interaction, check!); + return await kickCallback(interaction, member); + } case "ban": { + const check = await banCheck(interaction, false, member) + if (check !== true) return await errorMessage(interaction, check!); + return await banCallback(interaction, member); + } case "mute": { + const check = await muteCheck(interaction, false, member) + if (check !== true) return await errorMessage(interaction, check!); + return await muteCallback(interaction, member); + } case "nickname": { + const check = await nicknameCheck(interaction, false, member) + if (check !== true) return await errorMessage(interaction, check || "Something went wrong"); + return await nicknameCallback(interaction, member); + } case "warn": { + const check = await warnCheck(interaction, false, member) + if (check !== true) return await errorMessage(interaction, check!); + return await warnCallback(interaction, member); + } + } + } } } diff --git a/src/events/memberJoin.ts b/src/events/memberJoin.ts index bdfd999..191ba6b 100644 --- a/src/events/memberJoin.ts +++ b/src/events/memberJoin.ts @@ -2,12 +2,14 @@ import type { GuildMember } from "discord.js"; import { callback as statsChannelAdd } from "../reflex/statsChannelUpdate.js"; import { callback as welcome } from "../reflex/welcome.js"; import type { NucleusClient } from "../utils/client.js"; +import { doMemberChecks } from "../reflex/scanners.js"; export const event = "guildMemberAdd"; export async function callback(client: NucleusClient, member: GuildMember) { - welcome(client, member); - statsChannelAdd(client, member); + welcome(member); + statsChannelAdd(member.user, member.guild); + doMemberChecks(member, member.guild) const { log, isLogging, NucleusColors, entry, renderUser, renderDelta } = client.logger; if (!(await isLogging(member.guild.id, "guildMemberUpdate"))) return; await client.database.history.create("join", member.guild.id, member.user, null, null); diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 4c85052..0281fa1 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -30,7 +30,7 @@ export async function callback(_client: NucleusClient, message: Message) { if (message.author.bot) return; if (message.channel.isDMBased()) return; try { - await statsChannelUpdate(client, await message.guild.members.fetch(message.author.id)); + await statsChannelUpdate((await message.guild.members.fetch(message.author.id)).user, message.guild); } catch (e) { console.log(e); } diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts index 450a4af..f00e82c 100644 --- a/src/reflex/scanners.ts +++ b/src/reflex/scanners.ts @@ -8,6 +8,9 @@ import { createHash } from "crypto"; import * as nsfwjs from 'nsfwjs'; import * as clamscan from 'clamscan' import * as tf from "@tensorflow/tfjs-node"; +import EmojiEmbed from "../utils/generateEmojiEmbed.js"; +import getEmojiByName from "../utils/getEmojiByName.js"; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; interface NSFWSchema { nsfw: boolean; @@ -164,7 +167,8 @@ export async function MalwareCheck(element: string): Promise { } } -export function TestString(string: string, soft: string[], strict: string[]): object | null { +export function TestString(string: string, soft: string[], strict: string[], enabled?: boolean): {word: string, type: string} | null { + if (!enabled) return null; for (const word of strict) { if (string.toLowerCase().includes(word)) { return { word: word, type: "strict" }; @@ -173,7 +177,7 @@ export function TestString(string: string, soft: string[], strict: string[]): ob for (const word of soft) { for (const word2 of string.match(/[a-z]+/gi) ?? []) { if (word2 === word) { - return { word: word, type: "strict" }; + return { word: word, type: "soft" }; } } } @@ -188,3 +192,76 @@ export async function TestImage(url: string): Promise { }); return text; } + +export async function doMemberChecks(member: Discord.GuildMember, guild: Discord.Guild): Promise { + if (member.user.bot) return; + const guildData = await client.database.guilds.read(guild.id); + if (!guildData.logging.staff.channel) return; + const [ loose, strict ] = [guildData.filters.wordFilter.words.loose, guildData.filters.wordFilter.words.strict]; + // Does the username contain filtered words + const usernameCheck = TestString(member.user.username, loose, strict, guildData.filters.wordFilter.enabled); + // Does the nickname contain filtered words + const nicknameCheck = TestString(member.nickname ?? "", loose, strict, guildData.filters.wordFilter.enabled); + // Does the profile picture contain filtered words + const avatarTextCheck = TestString(await TestImage(member.user.displayAvatarURL({ forceStatic: true })) ?? "", loose, strict, guildData.filters.wordFilter.enabled); + // Is the profile picture NSFW + const avatarCheck = guildData.filters.images.NSFW && await NSFWCheck(member.user.displayAvatarURL({ forceStatic: true })); + // Does the username contain an invite + const inviteCheck = guildData.filters.invite.enabled && member.user.username.match(/discord\.gg\/[a-zA-Z0-9]+/gi) !== null; + // Does the nickname contain an invite + const nicknameInviteCheck = guildData.filters.invite.enabled && member.nickname?.match(/discord\.gg\/[a-zA-Z0-9]+/gi) !== null; + + if (usernameCheck !== null || nicknameCheck !== null || avatarCheck || inviteCheck || nicknameInviteCheck || avatarTextCheck !== null) { + const infractions = []; + if (usernameCheck !== null) { + infractions.push(`Username contains a ${usernameCheck.type}ly filtered word (${usernameCheck.word})`); + } if (nicknameCheck !== null) { + infractions.push(`Nickname contains a ${nicknameCheck.type}ly filtered word (${nicknameCheck.word})`); + } if (avatarCheck) { + infractions.push("Profile picture is NSFW"); + } if (inviteCheck) { + infractions.push("Username contains an invite"); + } if (nicknameInviteCheck) { + infractions.push("Nickname contains an invite"); + } if (avatarTextCheck !== null) { + infractions.push(`Profile picture contains a ${avatarTextCheck.type}ly filtered word: ${avatarTextCheck.word}`); + } + if (infractions.length === 0) return; + // This is bad - Warn in the staff notifications channel + const filter = getEmojiByName("ICONS.FILTER"); + const channel = guild.channels.cache.get(guildData.logging.staff.channel) as Discord.TextChannel; + const embed = new EmojiEmbed() + .setTitle("Member Flagged") + .setEmoji("ICONS.FLAGS.RED") + .setStatus("Danger") + .setDescription(`**Member:** ${member.user.username} (<@${member.user.id}>)\n\n` + + infractions.map((element) => `${filter} ${element}`).join("\n") + ) + await channel.send({ + embeds: [embed], + components: [new ActionRowBuilder().addComponents(...[ + new ButtonBuilder() + .setCustomId(`mod:warn:${member.user.id}`) + .setLabel("Warn") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId(`mod:mute:${member.user.id}`) + .setLabel("Mute") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId(`mod:kick:${member.user.id}`) + .setLabel("Kick") + .setStyle(ButtonStyle.Danger), + new ButtonBuilder() + .setCustomId(`mod:ban:${member.user.id}`) + .setLabel("Ban") + .setStyle(ButtonStyle.Danger), + ].concat((usernameCheck !== null || nicknameCheck !== null || avatarTextCheck !== null) ? [ + new ButtonBuilder() + .setCustomId(`mod:nickname:${member.user.id}`) + .setLabel("Change Name") + .setStyle(ButtonStyle.Primary) + ] : []))] + }); + } +} \ No newline at end of file diff --git a/src/reflex/statsChannelUpdate.ts b/src/reflex/statsChannelUpdate.ts index 6c601f7..e3c7a2a 100644 --- a/src/reflex/statsChannelUpdate.ts +++ b/src/reflex/statsChannelUpdate.ts @@ -1,7 +1,6 @@ import { getCommandMentionByName } from "../utils/getCommandDataByName.js"; import type { Guild, User } from "discord.js"; -import type { NucleusClient } from "../utils/client.js"; -import type { GuildMember } from "discord.js"; +import client from "../utils/client.js"; import convertCurlyBracketString from "../utils/convertCurlyBracketString.js"; import singleNotify from "../utils/singleNotify.js"; @@ -10,10 +9,8 @@ interface PropSchema { name: string; } -export async function callback(client: NucleusClient, member?: GuildMember, guild?: Guild, user?: User) { - if (!member && !guild) return; - guild = await client.guilds.fetch(member ? member.guild.id : guild!.id); - user = user ?? member!.user; +export async function callback(user: User, guild: Guild) { + guild = await client.guilds.fetch(guild.id); const config = await client.database.guilds.read(guild.id); Object.entries(config.stats).forEach(async ([channel, props]) => { if ((props as PropSchema).enabled) { @@ -22,16 +19,16 @@ export async function callback(client: NucleusClient, member?: GuildMember, guil string = await convertCurlyBracketString(string, user!.id, user!.username, guild!.name, guild!.members); let fetchedChannel; try { - fetchedChannel = await guild!.channels.fetch(channel); + fetchedChannel = await guild.channels.fetch(channel); } catch (e) { fetchedChannel = null; } if (!fetchedChannel) { const deleted = config.stats[channel]; - await client.database.guilds.write(guild!.id, null, `stats.${channel}`); + await client.database.guilds.write(guild.id, null, `stats.${channel}`); return singleNotify( "statsChannelDeleted", - guild!.id, + guild.id, `One or more of your stats channels have been deleted. You can use ${getCommandMentionByName( "settings/stats" )}.\n` + `The channels name was: ${deleted!.name}`, diff --git a/src/reflex/welcome.ts b/src/reflex/welcome.ts index 5597b81..8e471c6 100644 --- a/src/reflex/welcome.ts +++ b/src/reflex/welcome.ts @@ -1,12 +1,11 @@ import { getCommandMentionByName } from "./../utils/getCommandDataByName.js"; -import type { NucleusClient } from "../utils/client.js"; import convertCurlyBracketString from "../utils/convertCurlyBracketString.js"; import client from "../utils/client.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; import { GuildChannel, GuildMember, BaseGuildTextChannel } from "discord.js"; import singleNotify from "../utils/singleNotify.js"; -export async function callback(_client: NucleusClient, member: GuildMember) { +export async function callback(member: GuildMember) { if (member.user.bot) return; const config = await client.database.guilds.read(member.guild.id); if (!config.welcome.enabled) return; diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts index 05c5494..5bfbfdb 100644 --- a/src/utils/confirmationMessage.ts +++ b/src/utils/confirmationMessage.ts @@ -1,4 +1,4 @@ -import { TextInputBuilder } from "discord.js"; +import { ButtonInteraction, TextInputBuilder } from "discord.js"; import Discord, { CommandInteraction, Message, @@ -24,7 +24,7 @@ interface CustomBoolean { } class confirmationMessage { - interaction: CommandInteraction; + interaction: CommandInteraction | ButtonInteraction; title = ""; emoji = ""; redEmoji: string | null = null; @@ -37,7 +37,9 @@ class confirmationMessage { inverted = false; reason: string | null = null; - constructor(interaction: CommandInteraction) { + modals: {buttonText: string, emoji: string, customId: string, modal: Discord.ModalBuilder, value: string | undefined}[] = []; + + constructor(interaction: CommandInteraction | ButtonInteraction) { this.interaction = interaction; } @@ -98,11 +100,17 @@ class confirmationMessage { this.reason = reason; return this; } + addModal(buttonText: string, emoji: string, customId: string, current: string, modal: Discord.ModalBuilder) { + modal.setCustomId(customId); + this.modals.push({buttonText, emoji, customId, modal, value: current}); + return this; + } async send(editOnly?: boolean): Promise<{ success?: boolean; cancelled?: boolean; components?: Record>; newReason?: string; + modals?: {buttonText: string, emoji: string, customId: string, modal: Discord.ModalBuilder, value: string | undefined}[]; }> { let cancelled = false; let success: boolean | undefined = undefined; @@ -131,6 +139,16 @@ class confirmationMessage { if (v.emoji !== undefined) button.setEmoji(getEmojiByName(v.emoji, "id")); fullComponents.push(button); }); + for (const modal of this.modals) { + fullComponents.push( + new Discord.ButtonBuilder() + .setCustomId(modal.customId) + .setLabel(modal.buttonText) + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName(modal.emoji, "id")) + .setDisabled(false) + ); + } if (this.reason !== null) fullComponents.push( new Discord.ButtonBuilder() @@ -183,7 +201,6 @@ class confirmationMessage { m = (await this.interaction.reply(object)) as unknown as Message; } } catch (e) { - console.log(e); cancelled = true; continue; } @@ -195,7 +212,7 @@ class confirmationMessage { time: 300000 }); } catch (e) { - success = false; + success = false break; } if (component.customId === "yes") { @@ -273,6 +290,46 @@ class confirmationMessage { returnComponents = true; continue; } + } else if (this.modals.map((m) => m.customId).includes(component.customId)) { + const chosenModal = this.modals.find((component => m => m.customId === component.customId)(component)); + await component.showModal(chosenModal!.modal); + await this.interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setTitle(this.title) + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus(this.color) + .setEmoji(this.emoji) + ], + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + ) + ] + }); + let out; + try { + out = (await modalInteractionCollector( + m, + this.interaction.user + )) as Discord.ModalSubmitInteraction | null; + } catch (e) { + console.log(e); + cancelled = true; + continue; + } + if (out === null || out.isButton()) { + continue; + } + if (out instanceof ModalSubmitInteraction) { + chosenModal!.value = out.fields.getTextInputValue("default"); + } + returnComponents = true; + continue; } else { component.deferUpdate(); this.customButtons[component.customId]!.active = !this.customButtons[component.customId]!.active; @@ -297,17 +354,19 @@ class confirmationMessage { ], components: [] }); - return { success: false }; + return { success: false, cancelled: returnValue.cancelled ?? false }; } if (returnComponents || success !== undefined) returnValue.components = this.customButtons; if (success !== undefined) returnValue.success = success; if (newReason) returnValue.newReason = newReason; + returnValue.modals = this.modals; + const modals = this.modals; const typedReturnValue = returnValue as | { cancelled: true } - | { success: boolean; components: Record>; newReason?: string } - | { newReason: string; components: Record> } - | { components: Record> }; + | { success: boolean; components: Record>; modals: typeof modals; newReason?: string } + | { newReason: string; components: Record>; modals: typeof modals } + | { components: Record>; modals: typeof modals }; return typedReturnValue; } diff --git a/src/utils/database.ts b/src/utils/database.ts index 75a79d9..62aee7c 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -14,14 +14,17 @@ import * as crypto from "crypto"; import _ from "lodash"; import defaultData from "../config/default.js"; -const username = encodeURIComponent(config.mongoOptions.username); -const password = encodeURIComponent(config.mongoOptions.password); +let username, password; + +// @ts-expect-error +if (Object.keys(config.mongoOptions).includes("username")) username = encodeURIComponent(config.mongoOptions.username); +// @ts-expect-error +if (Object.keys(config.mongoOptions).includes("password")) password = encodeURIComponent(config.mongoOptions.password); const mongoClient = new MongoClient( username - ? `mongodb://${username}:${password}@${config.mongoOptions.host}?authMechanism=DEFAULT` - : `mongodb://${config.mongoOptions.host}`, - { authSource: config.mongoOptions.authSource } + ? `mongodb://${username}:${password}@${config.mongoOptions.host}?authMechanism=DEFAULT&authSource=${config.mongoOptions.authSource}` + : `mongodb://${config.mongoOptions.host}` ); await mongoClient.connect(); const database = mongoClient.db(); @@ -221,7 +224,7 @@ interface TranscriptSchema { interface findDocSchema { channelID: string; messageID: string; - code: string; + transcript: string; } export class Transcript { @@ -284,20 +287,16 @@ export class Transcript { async deleteAll(guild: string) { // console.log("Transcript delete") const filteredDocs = await this.transcripts.find({ guild: guild }).toArray(); - const filteredDocs1 = await this.messageToTranscript.find({ guild: guild }).toArray(); for (const doc of filteredDocs) { await this.transcripts.deleteOne({ code: doc.code }); } - for (const doc of filteredDocs1) { - await this.messageToTranscript.deleteOne({ code: doc.code }); - } } async readEncrypted(code: string) { // console.log("Transcript read") let doc: TranscriptSchema | null = await this.transcripts.findOne({ code: code }); let findDoc: findDocSchema | null = null; - if (!doc) findDoc = await this.messageToTranscript.findOne({ code: code }); + if (!doc) findDoc = await this.messageToTranscript.findOne({ transcript: code }); if (findDoc) { const message = await ( client.channels.cache.get(findDoc.channelID) as Discord.TextBasedChannel | null @@ -334,7 +333,7 @@ export class Transcript { let doc: TranscriptSchema | null = await this.transcripts.findOne({ code: code }); let findDoc: findDocSchema | null = null; console.log(doc); - if (!doc) findDoc = await this.messageToTranscript.findOne({ code: code }); + if (!doc) findDoc = await this.messageToTranscript.findOne({ transcript: code }); if (findDoc) { const message = await ( client.channels.cache.get(findDoc.channelID) as Discord.TextBasedChannel | null @@ -837,6 +836,10 @@ export class Premium { } } +export class Plugins { + +} + export interface GuildConfig { id: string; version: number;