From 5b53a8c56b2341b0979a05c3e8b24af6f348e4f4 Mon Sep 17 00:00:00 2001 From: TheCodedProf Date: Fri, 3 Feb 2023 15:40:26 -0500 Subject: [PATCH] finished automod --- src/commands/settings/automod.ts | 350 ++++++++++++++++++++++++++++++- src/events/messageDelete.ts | 2 +- src/reflex/scanners.ts | 33 ++- src/utils/log.ts | 3 +- 4 files changed, 360 insertions(+), 28 deletions(-) diff --git a/src/commands/settings/automod.ts b/src/commands/settings/automod.ts index a7b6dbc..8e006b0 100644 --- a/src/commands/settings/automod.ts +++ b/src/commands/settings/automod.ts @@ -1,10 +1,34 @@ import type Discord from "discord.js"; -import { ActionRowBuilder, AnyComponent, AnyComponentBuilder, AnySelectMenuInteraction, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, CommandInteraction, Guild, GuildMember, GuildTextBasedChannel, MentionableSelectMenuBuilder, Message, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SelectMenuBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, UserSelectMenuBuilder, UserSelectMenuInteraction } from "discord.js"; +import { ActionRowBuilder, + AnySelectMenuInteraction, + APIMessageComponentEmoji, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + ChannelSelectMenuBuilder, + ChannelSelectMenuInteraction, + CommandInteraction, + Interaction, + Message, + MessageComponentInteraction, + ModalBuilder, + ModalSubmitInteraction, + RoleSelectMenuBuilder, + RoleSelectMenuInteraction, + StringSelectMenuBuilder, + StringSelectMenuInteraction, + StringSelectMenuOptionBuilder, + TextInputBuilder, + TextInputStyle, + UserSelectMenuBuilder, + UserSelectMenuInteraction +} from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import { LoadingEmbed } from "../../utils/defaults.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; +import { modalInteractionCollector } from "../../utils/dualCollector.js"; const command = (builder: SlashCommandSubcommandBuilder) => @@ -75,7 +99,7 @@ const toSelectMenu = async (interaction: StringSelectMenuInteraction, m: Message let i: AnySelectMenuInteraction | ButtonInteraction; try { - i = await interaction.channel!.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000}); + i = await m.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000}); } catch(e) { closed = true; break; @@ -177,7 +201,152 @@ const wordMenu = async (interaction: StringSelectMenuInteraction, m: Message, cu }> => { let closed = false; do { - closed = true; + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("enabled") + .setLabel("Enabled") + .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji), + ); + + const selectMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("edit") + .setPlaceholder("Edit... ") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Words") + .setDescription("Edit your list of words to filter") + .setValue("words"), + new StringSelectMenuOptionBuilder() + .setLabel("Allowed Users") + .setDescription("Users who will be unaffected by the word filter") + .setValue("allowedUsers"), + new StringSelectMenuOptionBuilder() + .setLabel("Allowed Roles") + .setDescription("Roles that will be unaffected by the word filter") + .setValue("allowedRoles"), + new StringSelectMenuOptionBuilder() + .setLabel("Allowed Channels") + .setDescription("Channels where the word filter will not apply") + .setValue("allowedChannels") + ) + .setDisabled(!current.enabled) + ); + + const embed = new EmojiEmbed() + .setTitle("Word Filters") + .setDescription( + `${emojiFromBoolean(current.enabled)} **Enabled**\n` + + `**Strict Words:** ${listToAndMore(current.words.strict, 5)}\n` + + `**Loose Words:** ${listToAndMore(current.words.loose, 5)}\n\n` + + `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` + + `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` + + `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5) + ) + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + + await interaction.editReply({embeds: [embed], components: [selectMenu, buttons]}); + + let i: ButtonInteraction | StringSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + + if(i.isButton()) { + await i.deferUpdate(); + switch(i.customId) { + case "back": + closed = true; + break; + case "enabled": + current.enabled = !current.enabled; + break; + } + } else { + switch(i.values[0]) { + case "words": + await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Word Filter") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + ], components: [new ActionRowBuilder().addComponents(new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + )]}) + const modal = new ModalBuilder() + .setTitle("Word Filter") + .setCustomId("wordFilter") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("wordStrict") + .setLabel("Strict Words") + .setPlaceholder("Matches anywhere in the message, including surrounded by other characters") + .setValue(current.words.strict.join(", ") ?? "") + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + ), + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("wordLoose") + .setLabel("Loose Words") + .setPlaceholder("Matches only if the word is by itself, surrounded by spaces or punctuation") + .setValue(current.words.loose.join(", ") ?? "") + .setStyle(TextInputStyle.Paragraph) + .setRequired(false) + ) + ) + + await i.showModal(modal); + let out; + try { + out = await modalInteractionCollector( + m, + (m: Interaction) => + (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId, + (m) => m.customId === "back" + ); + } catch (e) { + break; + } + if (!out) break; + if(out.isButton()) break; + current.words.strict = out.fields.getTextInputValue("wordStrict") + .split(",").map(s => s.trim()).filter(s => s.length > 0); + current.words.loose = out.fields.getTextInputValue("wordLoose") + .split(",").map(s => s.trim()).filter(s => s.length > 0); + break; + case "allowedUsers": + await i.deferUpdate(); + current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Word Filter"); + break; + case "allowedRoles": + await i.deferUpdate(); + current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Word Filter"); + break; + case "allowedChannels": + await i.deferUpdate(); + current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Word Filter"); + break; + } + } } while(!closed); return current; } @@ -231,15 +400,15 @@ const inviteMenu = async (interaction: StringSelectMenuInteraction, m: Message, .setDescription( "Automatically deletes invites sent by users (outside of staff members and self promotion channels)" + `\n\n` + `${emojiFromBoolean(current.enabled)} **${current.enabled ? "Enabled" : "Disabled"}**\n\n` + - `**Users:** ` + listToAndMore(current.allowed.users.map(user => `> <@${user}>`), 5) + `\n` + - `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `> <@&${role}>`), 5) + `\n` + - `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `> <#${channel}>`), 5) + `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` + + `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` + + `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5) ) .setStatus("Success") .setEmoji("GUILD.SETTINGS.GREEN") - await interaction.editReply({embeds: [embed], components: [buttons, menu]}); + await interaction.editReply({embeds: [embed], components: [menu, buttons]}); let i: ButtonInteraction | StringSelectMenuInteraction; try { @@ -301,7 +470,172 @@ const mentionMenu = async (interaction: StringSelectMenuInteraction, m: Message, let closed = false; do { - closed = true; + + const buttons = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId("back") + .setLabel("Back") + .setStyle(ButtonStyle.Secondary) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("everyone") + .setLabel(current.everyone ? "Everyone" : "No one") + .setStyle(current.everyone ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.everyone, "id") as APIMessageComponentEmoji), + new ButtonBuilder() + .setCustomId("roles") + .setLabel(current.roles ? "Roles" : "No roles") + .setStyle(current.roles ? ButtonStyle.Success : ButtonStyle.Danger) + .setEmoji(emojiFromBoolean(current.roles, "id") as APIMessageComponentEmoji) + ); + const menu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("toEdit") + .setPlaceholder("Edit mention settings") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Mass Mention Amount") + .setDescription("The amount of mentions before the bot will delete the message") + .setValue("mass"), + new StringSelectMenuOptionBuilder() + .setLabel("Roles") + .setDescription("Roles that are able to be mentioned") + .setValue("roles"), + ) + ) + + const allowedMenu = new ActionRowBuilder() + .addComponents( + new StringSelectMenuBuilder() + .setCustomId("allowed") + .setPlaceholder("Edit exceptions") + .addOptions( + new StringSelectMenuOptionBuilder() + .setLabel("Users") + .setDescription("Users that are unaffected by the mention filter") + .setValue("users"), + new StringSelectMenuOptionBuilder() + .setLabel("Roles") + .setDescription("Roles that are unaffected by the mention filter") + .setValue("roles"), + new StringSelectMenuOptionBuilder() + .setLabel("Channels") + .setDescription("Channels where anyone is unaffected by the mention filter") + .setValue("channels") + ) + ) + + const embed = new EmojiEmbed() + .setTitle("Mention Settings") + .setDescription( + `Log when members mention:\n` + + `${emojiFromBoolean(true)} **${current.mass}+ members** in one message\n` + + `${emojiFromBoolean(current.everyone)} **Everyone**\n` + + `${emojiFromBoolean(current.roles)} **Roles**\n` + + (current.allowed.rolesToMention.length > 0 ? `> *Except for ${listToAndMore(current.allowed.rolesToMention.map(r => `<@&${r}>`), 3)}*\n` : "") + + "\n" + + `Except if...\n` + + `> ${current.allowed.users.length > 0 ? `Member is: ${listToAndMore(current.allowed.users.map(u => `<@${u}>`), 3)}\n` : ""}` + + `> ${current.allowed.roles.length > 0 ? `Member has role: ${listToAndMore(current.allowed.roles.map(r => `<@&${r}>`), 3)}\n` : ""}` + + `> ${current.allowed.channels.length > 0 ? `In channel: ${listToAndMore(current.allowed.channels.map(c => `<#${c}>`), 3)}\n` : ""}` + ) + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + + await interaction.editReply({embeds: [embed], components: [menu, allowedMenu, buttons]}); + + let i: ButtonInteraction | StringSelectMenuInteraction; + try { + i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction; + } catch (e) { + closed = true; + break; + } + + if(i.isButton()) { + await i.deferUpdate(); + switch (i.customId) { + case "back": + closed = true; + break; + case "everyone": + current.everyone = !current.everyone; + break; + case "roles": + current.roles = !current.roles; + break; + } + } else { + switch (i.customId) { + case "toEdit": + switch (i.values[0]) { + case "mass": + await interaction.editReply({embeds: [new EmojiEmbed() + .setTitle("Word Filter") + .setDescription("Modal opened. If you can't see it, click back and try again.") + .setStatus("Success") + .setEmoji("GUILD.SETTINGS.GREEN") + ], components: [new ActionRowBuilder().addComponents(new ButtonBuilder() + .setLabel("Back") + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Primary) + .setCustomId("back") + )]}) + const modal = new ModalBuilder() + .setTitle("Mass Mention Amount") + .setCustomId("mass") + .addComponents( + new ActionRowBuilder() + .addComponents( + new TextInputBuilder() + .setCustomId("mass") + .setPlaceholder("Amount") + .setMinLength(1) + .setMaxLength(3) + .setStyle(TextInputStyle.Short) + ) + ) + await i.showModal(modal); + let out; + try { + out = await modalInteractionCollector( + m, + (m: Interaction) => + (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId, + (m) => m.customId === "back" + ); + } catch (e) { + break; + } + if (!out) break; + if(out.isButton()) break; + current.mass = parseInt(out.fields.getTextInputValue("mass")); + break; + case "roles": + await i.deferUpdate(); + current.allowed.rolesToMention = await toSelectMenu(interaction, m, current.allowed.rolesToMention, "role", "Mention Settings"); + break; + } + break; + case "allowed": + await i.deferUpdate(); + switch (i.values[0]) { + case "users": + current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings"); + break; + case "roles": + current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings"); + break; + case "channels": + current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Mention Settings"); + break; + } + break; + } + } + } while(!closed); return current } diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 9563a33..65e1422 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -6,7 +6,7 @@ export const event = "messageDelete"; export async function callback(client: NucleusClient, message: Message) { if (message.author.id === client.user!.id) return; if (message.author.bot) return; - if (client.noLog.includes(`${message.id}/${message.channel.id}/${message.id}`)) return; + if (client.noLog.includes(`${message.guild!.id}/${message.channel.id}/${message.id}`)) return; const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger; const auditLog = (await getAuditLog(message.guild!, AuditLogEvent.MemberBanAdd)) .filter((entry: GuildAuditLogsEntry) => (entry.target! as User).id === message.author.id)[0]; diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts index c8d59f0..c55e463 100644 --- a/src/reflex/scanners.ts +++ b/src/reflex/scanners.ts @@ -9,19 +9,18 @@ import { createHash } from "crypto"; interface NSFWSchema { nsfw: boolean; + errored?: boolean; } interface MalwareSchema { safe: boolean; + errored?: boolean; } export async function testNSFW(link: string): Promise { const [p, hash] = await saveAttachment(link); - console.log("Checking an image") let alreadyHaveCheck = await client.database.scanCache.read(hash) if(alreadyHaveCheck) return { nsfw: alreadyHaveCheck.data }; - console.log("Was not in db") const data = new FormData(); - console.log(link); data.append("file", createReadStream(p)); const result = await fetch("https://unscan.p.rapidapi.com/", { method: "POST", @@ -31,13 +30,14 @@ export async function testNSFW(link: string): Promise { }, body: data }) - .then((response) => response.json() as Promise) + .then((response) => response.status === 200 ? response.json() as Promise : { nsfw: false, errored: true }) .catch((err) => { console.error(err); - return { nsfw: false }; + return { nsfw: false, errored: true }; }); - console.log(result); - client.database.scanCache.write(hash, result.nsfw); + if(!result.errored) { + client.database.scanCache.write(hash, result.nsfw); + } return { nsfw: result.nsfw }; } @@ -48,7 +48,6 @@ export async function testMalware(link: string): Promise { const data = new URLSearchParams(); let f = createReadStream(p); data.append("file", f.read(fs.statSync(p).size)); - console.log(link); const result = await fetch("https://unscan.p.rapidapi.com/malware", { method: "POST", headers: { @@ -57,18 +56,18 @@ export async function testMalware(link: string): Promise { }, body: data }) - .then((response) => response.json() as Promise) + .then((response) => response.status === 200 ? response.json() as Promise : { safe: true, errored: true }) .catch((err) => { console.error(err); - return { safe: true }; + return { safe: true, errored: true }; }); - console.log(result); - client.database.scanCache.write(hash, result.safe); + if (!result.errored) { + client.database.scanCache.write(hash, result.safe); + } return { safe: result.safe }; } export async function testLink(link: string): Promise<{ safe: boolean; tags: string[] }> { - console.log(link); let alreadyHaveCheck = await client.database.scanCache.read(link) if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data, tags: [] }; const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/link", { @@ -84,7 +83,6 @@ export async function testLink(link: string): Promise<{ safe: boolean; tags: str console.error(err); return { safe: true, tags: [] }; }); - console.log(scanned); client.database.scanCache.write(link, scanned.safe ?? true, []); return { safe: scanned.safe ?? true, @@ -93,10 +91,11 @@ export async function testLink(link: string): Promise<{ safe: boolean; tags: str } export async function saveAttachment(link: string): Promise<[string, string]> { - const image = (await fetch(link)).arrayBuffer().toString(); + const image = await (await fetch(link)).arrayBuffer() const fileName = generateFileName(link.split("/").pop()!.split(".").pop()!); - writeFileSync(fileName, image, "base64"); - return [fileName, createHash('sha512').update(image, 'base64').digest('base64')]; + const enc = new TextDecoder("utf-8"); + writeFileSync(fileName, new DataView(image), "base64"); + return [fileName, createHash('sha512').update(enc.decode(image), 'base64').digest('base64')]; } const linkTypes = { diff --git a/src/utils/log.ts b/src/utils/log.ts index 4eff4e2..c5c7e06 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -10,7 +10,7 @@ const wait = promisify(setTimeout); export const Logger = { renderUser(user: Discord.User | string) { - if (typeof user === "string") return `${user} [<@${user}>]`; + if (typeof user === "string") user = client.users.cache.get(user) as Discord.User; return `${user.username} [<@${user.id}>]`; }, renderTime(t: number) { @@ -55,7 +55,6 @@ export const Logger = { const config = await client.database.guilds.read(log.hidden.guild); if (!config.logging.logs.enabled) return; if (!toHexArray(config.logging.logs.toLog).includes(log.meta.calculateType)) { - console.log("Not logging this type of event"); return; } if (config.logging.logs.channel) {