diff --git a/package.json b/package.json index 2f63ea6..6457877 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "dependencies": { - "@discordjs/rest": "^0.2.0-canary.0", "@hokify/agenda": "^6.2.12", + "@tensorflow/tfjs": "^4.0.0", + "@tensorflow/tfjs-node": "^4.2.0", "@total-typescript/ts-reset": "^0.3.7", "@tsconfig/node18-strictest-esm": "^1.0.0", "@types/node": "^18.14.6", "@ungap/structured-clone": "^1.0.1", "agenda": "^4.3.0", "body-parser": "^1.20.0", + "canvas": "^2.11.0", + "clamscan": "^2.1.2", "discord.js": "^14.7.1", "eslint": "^8.21.0", "express": "^4.18.1", @@ -18,12 +21,11 @@ "mongodb": "^4.7.0", "node-fetch": "^3.3.0", "node-tesseract-ocr": "^2.2.1", + "nsfwjs": "2.4.2", + "seedrandom": "^3.0.5", "structured-clone": "^0.2.2", "systeminformation": "^5.17.3" }, - "resolutions": { - "discord-api-types": "0.37.23" - }, "name": "nucleus", "version": "0.0.1", "description": "Nucleus: The core of your server", @@ -38,7 +40,8 @@ "lint-list": "echo 'Style checking...'; prettier --check .; echo 'Linting...'; eslint src; echo 'To view errors in more detail, please run `yarn lint`'; true", "lint-ci": "echo 'Style checking...' && prettier --check . && echo 'Linting...' && eslint src", "setup": "node Installer.js", - "win-force-build": "clear | rm -r dist | tsc-suppress" + "win-force-build": "clear | rm -r dist | tsc-suppress", + "audit-fix": "yarn-audit-fix" }, "repository": { "type": "git", @@ -57,6 +60,7 @@ "private": false, "type": "module", "devDependencies": { + "@types/clamscan": "^2.0.4", "@types/lodash": "^4.14.191", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", @@ -64,6 +68,7 @@ "prettier": "^2.7.1", "prettier-eslint": "^15.0.1", "tsc-suppress": "^1.0.7", - "typescript": "^4.9.4" + "typescript": "^4.9.4", + "yarn-audit-fix": "^9.3.9" } } diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban.ts index 628b607..91e074d 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", @@ -110,26 +115,19 @@ const callback = async (interaction: CommandInteraction): Promise => { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(config.moderation.ban.text) - .setURL( - config.moderation.ban.link.replaceAll( - "{id}", - (interaction.options.getMember("user") as GuildMember).id - ) - ) + .setURL(config.moderation.ban.link.replaceAll("{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 +187,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..4ef78c8 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( @@ -98,24 +103,18 @@ const callback = async (interaction: CommandInteraction): Promise => { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(config.moderation.kick.text) - .setURL( - config.moderation.kick.link.replaceAll( - "{id}", - (interaction.options.getMember("user") as GuildMember).id - ) - ) + .setURL(config.moderation.kick.link.replaceAll("{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 +185,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 +194,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..2266a1a 100644 --- a/src/commands/mod/mute.ts +++ b/src/commands/mod/mute.ts @@ -1,5 +1,12 @@ 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 +56,25 @@ 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) @@ -197,8 +213,7 @@ const callback = async (interaction: CommandInteraction): Promise => { "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), - async () => - await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, reason), + async () => await create(interaction.guild!, member!.user, interaction.user, reason), "An appeal ticket will be created when Confirm is clicked", null, "CONTROL.TICKET", @@ -275,12 +290,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 +409,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..cfdcf47 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -1,5 +1,16 @@ 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,44 +28,40 @@ 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")!), - "new nickname": `${ - (interaction.options.get("name")?.value as string) - ? (interaction.options.get("name")?.value as string) - : "*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?` + user: renderUser(member.user), + "new nickname": `${newNickname ? newNickname : "*No nickname*"}` + }) + `Are you sure you want to ${newNickname ? "change" : "clear"} <@!${member.id}>'s nickname?` ) .setColor("Danger") .addCustomBoolean( "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild!.id)), - async () => - await create( - interaction.guild!, - interaction.options.getUser("user")!, - interaction.user, - "Nickname changed" - ), + async () => await create(interaction.guild!, member!.user, interaction.user, "Nickname changed"), "An appeal ticket will be created", null, "CONTROL.TICKET", @@ -70,6 +77,23 @@ 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 +103,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; @@ -95,12 +120,8 @@ const callback = async (interaction: CommandInteraction): Promise => { .setEmoji("PUNISH.NICKNAME.RED") .setTitle("Nickname changed") .setDescription( - `Your nickname was ${ - (interaction.options.get("name")?.value as string) ? "changed" : "cleared" - } in ${interaction.guild!.name}.` + - ((interaction.options.get("name")?.value as string) - ? `\nIt is now: ${interaction.options.get("name")?.value as string}` - : "") + + `Your nickname was ${newNickname ? "changed" : "cleared"} in ${interaction.guild!.name}.` + + (newNickname ? `\nIt is now: ${newNickname}` : "") + "\n\n" + (createAppealTicket ? `You can appeal this in the ticket created in <#${ @@ -119,29 +140,20 @@ const callback = async (interaction: CommandInteraction): Promise => { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(config.moderation.nick.text) - .setURL( - config.moderation.nick.link.replaceAll( - "{id}", - (interaction.options.getMember("user") as GuildMember).id - ) - ) + .setURL(config.moderation.nick.link.replaceAll("{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 +161,7 @@ const callback = async (interaction: CommandInteraction): Promise => { interaction.user, null, before, - nickname + newNickname ); } catch { await interaction.editReply({ @@ -175,9 +187,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 +222,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/purge.ts b/src/commands/mod/purge.ts index ff69079..dadab04 100644 --- a/src/commands/mod/purge.ts +++ b/src/commands/mod/purge.ts @@ -101,8 +101,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let component; try { component = m.awaitMessageComponent({ - filter: (i) => - i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id, + filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id, time: 300000 }); } catch (e) { diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts index 5d1bd94..232219b 100644 --- a/src/commands/mod/warn.ts +++ b/src/commands/mod/warn.ts @@ -1,4 +1,11 @@ -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 +21,14 @@ 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,17 +42,16 @@ 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( "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), - async () => - await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, reason), + async () => await create(interaction.guild!, member!.user, interaction.user, reason), "An appeal ticket will be created", null, "CONTROL.TICKET", @@ -108,16 +119,11 @@ const callback = async (interaction: CommandInteraction): Promise => { new ButtonBuilder() .setStyle(ButtonStyle.Link) .setLabel(config.moderation.warn.text) - .setURL( - config.moderation.warn.link.replaceAll( - "{id}", - (interaction.options.getMember("user") as GuildMember).id - ) - ) + .setURL(config.moderation.warn.link.replaceAll("{id}", member.id)) ) ); } - await (interaction.options.getMember("user") as GuildMember).send(messageData); + await member.send(messageData); dmSent = true; } } catch (e) { @@ -133,10 +139,7 @@ const callback = async (interaction: CommandInteraction): Promise => { timestamp: Date.now() }, list: { - user: entry( - (interaction.options.getMember("user") as GuildMember).user.id, - renderUser((interaction.options.getMember("user") as GuildMember).user) - ), + user: entry(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*" }, @@ -149,13 +152,7 @@ const callback = async (interaction: CommandInteraction): Promise => { guild: interaction.guild.id } }; - await client.database.history.create( - "warn", - interaction.guild.id, - (interaction.options.getMember("user") as GuildMember).user, - interaction.user, - reason - ); + await client.database.history.create("warn", interaction.guild.id, member.user, interaction.user, reason); log(data); const failed = !dmSent && notify; if (!failed) { @@ -177,9 +174,7 @@ const callback = async (interaction: CommandInteraction): Promise => { components: [] }); } else { - const canSeeChannel = (interaction.options.getMember("user") as GuildMember) - .permissionsIn(interaction.channel as Discord.TextChannel) - .has("ViewChannel"); + const canSeeChannel = member.permissionsIn(interaction.channel as Discord.TextChannel).has("ViewChannel"); const m = (await interaction.editReply({ embeds: [ new EmojiEmbed() @@ -235,9 +230,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 +266,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 +297,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..294ee27 100644 --- a/src/commands/nucleus/stats.ts +++ b/src/commands/nucleus/stats.ts @@ -1,4 +1,18 @@ -import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, ChannelType, CommandInteraction, ComponentType, Guild, ModalBuilder, ModalSubmitInteraction, TextInputBuilder, TextInputStyle } from "discord.js"; +import { + ActionRowBuilder, + AttachmentBuilder, + ButtonBuilder, + ButtonInteraction, + ButtonStyle, + ChannelType, + CommandInteraction, + ComponentType, + Guild, + ModalBuilder, + ModalSubmitInteraction, + TextInputBuilder, + TextInputStyle +} from "discord.js"; import type { SlashCommandSubcommandBuilder } from "discord.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import client from "../../utils/client.js"; @@ -8,7 +22,7 @@ const command = (builder: SlashCommandSubcommandBuilder) => builder.setName("stats").setDescription("Gets the bot's stats"); const callback = async (interaction: CommandInteraction): Promise => { - const description = `**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\`` + const description = `**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\``; const m = await interaction.reply({ embeds: [ new EmojiEmbed() @@ -28,27 +42,39 @@ 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() .addComponents( - new ActionRowBuilder() - .addComponents( - new TextInputBuilder() - .setStyle(TextInputStyle.Short) - .setLabel("Guild ID") - .setCustomId("guildID") - .setPlaceholder("Guild ID") - .setMinLength(16) - .setMaxLength(25) - ) + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setStyle(TextInputStyle.Short) + .setLabel("Guild ID") + .setCustomId("guildID") + .setPlaceholder("Guild ID") + .setMinLength(16) + .setMaxLength(25) + ) ) .setTitle("Admin Panel") - .setCustomId("adminPanel") + .setCustomId("adminPanel"); let i1: ButtonInteraction; - const channel = await client.channels.fetch(interaction.channelId) - if(!channel || [ChannelType.GuildCategory, ChannelType.GroupDM, ChannelType.GuildStageVoice].includes(channel.type)) return; + const channel = await client.channels.fetch(interaction.channelId); + if ( + !channel || + [ChannelType.GuildCategory, ChannelType.GroupDM, ChannelType.GuildStageVoice].includes(channel.type) + ) + return; // console.log(interaction) if (!("awaitMessageComponent" in channel)) return; try { @@ -56,27 +82,28 @@ const callback = async (interaction: CommandInteraction): Promise => { filter: (i) => i.customId === "admin" && i.user.id === interaction.user.id, time: 300000 }); - } catch (e) { console.log(e); return } - await i1.showModal(modal) + } catch (e) { + console.log(e); + return; + } + await i1.showModal(modal); let out: ModalSubmitInteraction; try { out = await i1.awaitModalSubmit({ filter: (i) => i.customId === "adminPanel" && i.user.id === interaction.user.id, time: 300000 - }) - } catch { return } + }); + } catch { + return; + } out.deferUpdate(); const GuildID = out.fields.getTextInputValue("guildID"); if (!client.guilds.cache.has(GuildID)) { await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Admin") - .setDescription("Not in server") - .setStatus("Danger") - ], components: [] + embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")], + components: [] }); - }; + } await interaction.editReply({ embeds: [], @@ -88,24 +115,23 @@ const callback = async (interaction: CommandInteraction): Promise => { new ButtonBuilder().setCustomId("purge").setLabel("Delete data").setStyle(ButtonStyle.Danger), new ButtonBuilder().setCustomId("cache").setLabel("Reset cache").setStyle(ButtonStyle.Success) ) - ]}); + ] + }); let i; try { i = await m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id, time: 300000 - }) - } catch { return } + }); + } catch { + return; + } i.deferUpdate(); - const guild = await client.guilds.fetch(GuildID) as Guild | null; + const guild = (await client.guilds.fetch(GuildID)) as Guild | null; if (!guild) { await interaction.editReply({ - embeds: [ - new EmojiEmbed() - .setTitle("Admin") - .setDescription("Not in server") - .setStatus("Danger") - ], components: [] + embeds: [new EmojiEmbed().setTitle("Admin").setDescription("Not in server").setStatus("Danger")], + components: [] }); return; } @@ -116,17 +142,17 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle("Stats") .setDescription( `**Name:** ${guild.name}\n` + - `**ID:** \`${guild.id}\`\n` + - `**Owner:** ${client.users.cache.get(guild.ownerId)!.tag}\n` + - `**Member Count:** ${guild.memberCount}\n` + - `**Created:** \n` + - `**Added Nucleus:** \n` + - `**Nucleus' Perms:** https://discordapi.com/permissions.html#${guild.members.me!.permissions.valueOf()}\n` + `**ID:** \`${guild.id}\`\n` + + `**Owner:** ${client.users.cache.get(guild.ownerId)!.tag}\n` + + `**Member Count:** ${guild.memberCount}\n` + + `**Created:** \n` + + `**Added Nucleus:** \n` + + `**Nucleus' Perms:** https://discordapi.com/permissions.html#${guild.members.me!.permissions.valueOf()}\n` ) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") ] - }) + }); } else if (i.customId === "leave") { await guild.leave(); await interaction.editReply({ @@ -136,8 +162,9 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription(`Left ${guild.name}`) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ], components: [] - }) + ], + components: [] + }); } else if (i.customId === "data") { // Get all the data and convert to a string const data = await client.database.guilds.read(guild.id); @@ -147,9 +174,10 @@ const callback = async (interaction: CommandInteraction): Promise => { await interaction.editReply({ embeds: [ new EmojiEmbed().setTitle("Data").setDescription(`Data for ${guild.name}`).setStatus("Success") - ], components: [], + ], + components: [], files: [attachment] - }) + }); } else if (i.customId === "purge") { await client.database.guilds.delete(GuildID); await client.database.history.delete(GuildID); @@ -162,8 +190,9 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription(`Deleted data for ${guild.name}`) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ], components: [] - }) + ], + components: [] + }); } else if (i.customId === "cache") { await client.memory.forceUpdate(guild.id); await interaction.editReply({ @@ -173,8 +202,9 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription(`Reset cache for ${guild.name}`) .setStatus("Success") .setEmoji("SETTINGS.STATS.GREEN") - ], components: [] - }) + ], + components: [] + }); } } }; 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/commands/settings/rolemenu.ts b/src/commands/settings/rolemenu.ts index 0c174f5..e683e4f 100644 --- a/src/commands/settings/rolemenu.ts +++ b/src/commands/settings/rolemenu.ts @@ -176,7 +176,7 @@ const editRoleMenuPage = async ( m: Message, data?: ObjectSchema ): Promise => { - if (!data) data = _.cloneDeep(defaultRoleMenuData) + if (!data) data = _.cloneDeep(defaultRoleMenuData); const buttons = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("back") 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..34a52b8 100644 --- a/src/events/guildMemberUpdate.ts +++ b/src/events/guildMemberUpdate.ts @@ -2,6 +2,7 @@ import { AuditLogEvent, GuildAuditLogsEntry, GuildMember } from "discord.js"; import type { NucleusClient } from "../utils/client.js"; import type { LoggerOptions } from "../utils/log.js"; import { generalException } from "../utils/createTemporaryStorage.js"; +import { doMemberChecks } from "../reflex/scanners.js"; export const event = "guildMemberUpdate"; @@ -92,6 +93,7 @@ export async function callback(client: NucleusClient, before: GuildMember, after if (!auditLog) return; if (auditLog.executor!.id === client.user!.id) return; if (before.nickname !== after.nickname) { + doMemberChecks(after, after.guild); await client.database.history.create( "nickname", after.guild.id, @@ -111,7 +113,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..3b0cd62 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -4,13 +4,27 @@ 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 +50,39 @@ 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..66abe28 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 71e63b4..192be25 100644 --- a/src/reflex/scanners.ts +++ b/src/reflex/scanners.ts @@ -5,6 +5,12 @@ import Tesseract from "node-tesseract-ocr"; import type Discord from "discord.js"; import client from "../utils/client.js"; 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; @@ -15,32 +21,18 @@ interface MalwareSchema { errored?: boolean; } +// const model = await nsfwjs.load(); + export async function testNSFW(link: string): Promise { - const [p, hash] = await saveAttachment(link); + const [_fileName, hash] = await saveAttachment(link); const alreadyHaveCheck = await client.database.scanCache.read(hash); if (alreadyHaveCheck) return { nsfw: alreadyHaveCheck.data }; - const data = new URLSearchParams(); - const r = createReadStream(p); - data.append("file", r.read(fs.statSync(p).size)); - const result = await fetch("https://unscan.p.rapidapi.com/", { - method: "POST", - headers: { - "X-RapidAPI-Key": client.config.rapidApiKey, - "X-RapidAPI-Host": "unscan.p.rapidapi.com" - }, - body: data - }) - .then((response) => - response.status === 200 ? (response.json() as Promise) : { nsfw: false, errored: true } - ) - .catch((err) => { - console.error(err); - return { nsfw: false, errored: true }; - }); - if (!result.errored) { - client.database.scanCache.write(hash, result.nsfw); - } - return { nsfw: result.nsfw }; + + // const image = tf.node.decodePng() + + // const result = await model.classify(image) + + return { nsfw: false }; } export async function testMalware(link: string): Promise { @@ -175,7 +167,13 @@ 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" }; @@ -184,7 +182,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" }; } } } @@ -199,3 +197,107 @@ 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) + ] + : [] + ) + ) + ] + }); + } +} 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..7b3bd31 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,15 @@ 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 +106,23 @@ 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 +151,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 +213,6 @@ class confirmationMessage { m = (await this.interaction.reply(object)) as unknown as Message; } } catch (e) { - console.log(e); cancelled = true; continue; } @@ -273,6 +302,51 @@ 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 +371,24 @@ 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..19aa00b 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,8 @@ export class Premium { } } +// export class Plugins {} + export interface GuildConfig { id: string; version: number;