diff --git a/TODO.json b/TODO.json index b4b2524..d95afcb 100644 --- a/TODO.json +++ b/TODO.json @@ -1,4 +1,15 @@ { + "logging": { + "logs": { + "enabled": true, + "toLog": "3fffff", + "ignore": { + "users": [], + "roles": [], + "channels": [] + } + } + }, "filters": { "images": { "NSFW": false, @@ -6,9 +17,12 @@ }, "malware": false, "wordFilter": { - "enabled": false, + "enabled": true, "words": { - "strict": [], + "strict": [ + "meat", + "noon" + ], "loose": [] }, "allowed": { @@ -37,23 +51,10 @@ } } }, - "welcome": { - "enabled": false, - "welcomeRole": null, - "channel": null, - "message": null - }, - "stats": [ - { - "enabled": true, - "channel": "9849175359", - "text": "{count} members | {count:bots} bots | {count:humans} humans" - } - ], "moderation": { "mute": { "timeout": true, - "role": null, + "role": "934941408849186856", "text": null, "link": null }, @@ -70,25 +71,24 @@ "link": null }, "warn": { - "text": null, - "link": null + "text": "Test", + "link": "https://google.com" }, "role": { "role": null } }, - "tracks": [ - ], - "logging": { - "logs": { - "enabled": true, - "toLog": "3fffff" - } - }, - "roleMenu": { - "enabled": true, - "allowWebUI": true, - "options": [ - ] + "roleMenu": [], + "stats": [], + "tracks": [], + "welcome": { + "enabled": false, + "verificationRequired": { + "message": null, + "role": null + }, + "welcomeRole": null, + "channel": null, + "message": null } } \ No newline at end of file diff --git a/package.json b/package.json index 0320d64..ca786a7 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "body-parser": "^1.20.0", "discord.js": "13.8.1", "express": "^4.18.1", + "fuse.js": "^6.6.2", "humanize": "^0.0.9", "humanize-duration": "^3.27.1", "jshaiku": "file:../haiku", diff --git a/src/actions/createModActionTicket.ts b/src/actions/createModActionTicket.ts index 0162523..40f99b9 100644 --- a/src/actions/createModActionTicket.ts +++ b/src/actions/createModActionTicket.ts @@ -5,7 +5,6 @@ import client from "../utils/client.js"; export async function create(guild: Discord.Guild, member: Discord.User, createdBy: Discord.User, reason: string) { let config = await client.database.guilds.read(guild.id); - // @ts-ignore const { log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger let overwrites = [{ id: member, diff --git a/src/actions/tickets/delete.ts b/src/actions/tickets/delete.ts index 1985a08..c14cf0c 100644 --- a/src/actions/tickets/delete.ts +++ b/src/actions/tickets/delete.ts @@ -134,29 +134,27 @@ async function purgeByUser(member, guild) { } }); if (deleted) { - try { - const { log, NucleusColors, entry, renderUser, renderDelta } = member.client.logger - let data = { - meta:{ - type: 'ticketPurge', - displayName: 'Tickets Purged', - calculateType: "ticketUpdate", - color: NucleusColors.red, - emoji: 'GUILD.TICKET.DELETE', - timestamp: new Date().getTime() - }, - list: { - ticketFor: entry(member, renderUser(member)), - deletedBy: entry(null, "Member left server"), - deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), - ticketsDeleted: deleted, - }, - hidden: { - guild: guild.id - } + const { log, NucleusColors, entry, renderUser, renderDelta } = member.client.logger + let data = { + meta:{ + type: 'ticketPurge', + displayName: 'Tickets Purged', + calculateType: "ticketUpdate", + color: NucleusColors.red, + emoji: 'GUILD.TICKET.DELETE', + timestamp: new Date().getTime() + }, + list: { + ticketFor: entry(member, renderUser(member)), + deletedBy: entry(null, "Member left server"), + deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), + ticketsDeleted: deleted, + }, + hidden: { + guild: guild.id } - log(data); - } catch {} + } + log(data); } } diff --git a/src/api/index.ts b/src/api/index.ts index c429452..5cc4a99 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -89,7 +89,6 @@ const runServer = (client: HaikuClient) => { const secret = req.body.secret; const data = req.body.data; if (secret === client.config.verifySecret) { - console.table(data) let guild = await client.guilds.fetch(client.roleMenu[code].guild); if (!guild) { return res.status(404) } let member = await guild.members.fetch(client.roleMenu[code].user); diff --git a/src/commands/categorisationTest.ts b/src/commands/categorisationTest.ts index f89a899..b052783 100644 --- a/src/commands/categorisationTest.ts +++ b/src/commands/categorisationTest.ts @@ -6,7 +6,7 @@ import client from "../utils/client.js" import addPlural from "../utils/plurals.js"; import getEmojiByName from "../utils/getEmojiByName.js"; -const command = new SlashCommandBuilder() +const command = new SlashCommandBuilder() // TODO: remove for release .setName("categorise") .setDescription("Categorises your servers channels") @@ -44,10 +44,8 @@ const callback = async (interaction: CommandInteraction): Promise => { for (let c of channels) { // convert channel to a channel if its a string let channel: any - console.log(c) if (typeof c === "string") channel = interaction.guild.channels.cache.get(channel).id - // @ts-ignore - else channel = c[0].id + else channel = (c[0] as unknown as GuildChannel).id console.log(channel) if (!predicted[channel]) predicted[channel] = [] m = await interaction.editReply({embeds: [new EmojiEmbed() diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban.ts index 0239951..2068b15 100644 --- a/src/commands/mod/ban.ts +++ b/src/commands/mod/ban.ts @@ -12,16 +12,14 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("ban") .setDescription("Bans a user from the server") .addUserOption(option => option.setName("user").setDescription("The user to ban").setRequired(true)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are banned | Default: Yes").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]]) - ) - .addIntegerOption(option => option.setName("delete").setDescription("The days of messages to delete | Default: 0").setMinValue(0).setMaxValue(7).setRequired(false)) + .addNumberOption(option => option.setName("delete").setDescription("The days of messages to delete | Default: 0").setMinValue(0).setMaxValue(7).setRequired(false)) const callback = async (interaction: CommandInteraction): Promise => { const { renderUser } = client.logger // TODO:[Modals] Replace this with a modal let reason = null - let confirmation + let notify = true; + let confirmation; while (true) { confirmation = await new confirmationMessage(interaction) .setEmoji("PUNISH.BAN.RED") @@ -30,22 +28,24 @@ const callback = async (interaction: CommandInteraction): Promise => { "user": renderUser(interaction.options.getUser("user")), "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*" }) - + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n` + + `The user **will${notify ? '' : ' not'}** be notified\n` + `${addPlurals(interaction.options.getInteger("delete") ? interaction.options.getInteger("delete") : 0, "day")} of messages will be deleted\n\n` + `Are you sure you want to ban <@!${(interaction.options.getMember("user") as GuildMember).id}>?`) .setColor("Danger") .addReasonButton(reason ?? "") .send(reason !== null) reason = reason ?? "" - if (confirmation.newReason === undefined) break - reason = confirmation.newReason + if (confirmation.cancelled) return + if (confirmation.success) break + if (confirmation.newReason) reason = confirmation.newReason + if (confirmation.components) notify = confirmation.components.notify.active } if (confirmation.success) { let dmd = false let dm; let config = await client.database.guilds.read(interaction.guild.id); try { - if (interaction.options.getString("notify") != "no") { + if (notify) { dm = await (interaction.options.getMember("user") as GuildMember).send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.BAN.RED") @@ -66,7 +66,7 @@ const callback = async (interaction: CommandInteraction): Promise => { try { let member = (interaction.options.getMember("user") as GuildMember) member.ban({ - days: Number(interaction.options.getInteger("delete") ?? 0), + days: Number(interaction.options.getNumber("delete") ?? 0), reason: reason ?? "No reason provided" }) try { await client.database.history.create("ban", interaction.guild.id, member.user, interaction.user, reason) } catch {} @@ -104,7 +104,7 @@ const callback = async (interaction: CommandInteraction): Promise => { if (dmd) await dm.delete() return } - let failed = (dmd == false && interaction.options.getString("notify") != "no") + let failed = (dmd == false && notify) await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.BAN.${failed ? "YELLOW" : "GREEN"}`) .setTitle(`Ban`) diff --git a/src/commands/mod/info.ts b/src/commands/mod/info.ts index b7f4b74..0ea93d8 100644 --- a/src/commands/mod/info.ts +++ b/src/commands/mod/info.ts @@ -14,7 +14,6 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Shows moderator information about a user") .addUserOption(option => option.setName("user").setDescription("The user to get information about").setRequired(true)) - const types = { "warn": {emoji: "PUNISH.WARN.YELLOW", text: "Warned"}, "mute": {emoji: "PUNISH.MUTE.YELLOW", text: "Muted"}, @@ -237,8 +236,7 @@ const callback = async (interaction: CommandInteraction): Promise => { } catch (e) { return } if (i.customId === "modify") { await i.showModal(new Discord.Modal().setCustomId("modal").setTitle(`Editing moderator note`).addComponents( - // @ts-ignore - new MessageActionRow().addComponents(new TextInputComponent() + new MessageActionRow().addComponents(new TextInputComponent() .setCustomId("note") .setLabel("Note") .setMaxLength(4000) diff --git a/src/commands/mod/kick.ts b/src/commands/mod/kick.ts index 793a630..eac7ca3 100644 --- a/src/commands/mod/kick.ts +++ b/src/commands/mod/kick.ts @@ -12,14 +12,12 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("kick") .setDescription("Kicks a user from the server") .addUserOption(option => option.setName("user").setDescription("The user to kick").setRequired(true)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are kicked | Default: Yes").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]]) - ) const callback = async (interaction: CommandInteraction): Promise => { const { renderUser } = client.logger // TODO:[Modals] Replace this with a modal let reason = null; + let notify = true; let confirmation while (true) { confirmation = await new confirmationMessage(interaction) @@ -29,21 +27,25 @@ const callback = async (interaction: CommandInteraction): Promise => { "user": renderUser(interaction.options.getUser("user")), "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*" }) - + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n` + + `The user **will${notify ? '' : ' not'}** be notified\n\n` + `Are you sure you want to kick <@!${(interaction.options.getMember("user") as GuildMember).id}>?`) .setColor("Danger") .addReasonButton(reason ?? "") .send(reason !== null) reason = reason ?? "" - if (confirmation.newReason === undefined) break - reason = confirmation.newReason + if (confirmation.cancelled) return + if (confirmation.success) break + if (confirmation.newReason) reason = confirmation.newReason + if (confirmation.components) { + notify = confirmation.components.notify.active + } } if (confirmation.success) { let dmd = false let dm; let config = await client.database.guilds.read(interaction.guild.id); try { - if (interaction.options.getString("notify") != "no") { + if (notify) { dm = await (interaction.options.getMember("user") as GuildMember).send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.KICK.RED") @@ -65,8 +67,7 @@ const callback = async (interaction: CommandInteraction): Promise => { (interaction.options.getMember("user") as GuildMember).kick(reason ?? "No reason provided.") let member = (interaction.options.getMember("user") as GuildMember) try { await client.database.history.create("kick", interaction.guild.id, member.user, interaction.user, reason) } catch {} - // @ts-ignore - const { log, NucleusColors, entry, renderUser, renderDelta } = member.client.logger + const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger let data = { meta: { type: 'memberKick', @@ -102,7 +103,7 @@ const callback = async (interaction: CommandInteraction): Promise => { if (dmd) await dm.delete() return } - let failed = (dmd == false && interaction.options.getString("notify") != "no") + let failed = (dmd == false && notify) await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.KICK.${failed ? "YELLOW" : "GREEN"}`) .setTitle(`Kick`) diff --git a/src/commands/mod/mute.ts b/src/commands/mod/mute.ts index 5e1a18b..f98bd6a 100644 --- a/src/commands/mod/mute.ts +++ b/src/commands/mod/mute.ts @@ -18,8 +18,6 @@ const command = (builder: SlashCommandSubcommandBuilder) => .addIntegerOption(option => option.setName("hours").setDescription("The number of hours to mute the user for | Default: 0").setMinValue(0).setMaxValue(23).setRequired(false)) .addIntegerOption(option => option.setName("minutes").setDescription("The number of minutes to mute the user for | Default: 0").setMinValue(0).setMaxValue(59).setRequired(false)) .addIntegerOption(option => option.setName("seconds").setDescription("The number of seconds to mute the user for | Default: 0").setMinValue(0).setMaxValue(59).setRequired(false)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are muted | Default: yes").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]])) const callback = async (interaction: CommandInteraction): Promise => { const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger @@ -119,6 +117,8 @@ const callback = async (interaction: CommandInteraction): Promise => { } // TODO:[Modals] Replace this with a modal let reason = null; + let notify = true; + let createAppealTicket = false; let confirmation; while (true) { confirmation = await new confirmationMessage(interaction) @@ -130,32 +130,39 @@ const callback = async (interaction: CommandInteraction): Promise => { "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*" }) + `The user will be ` + serverSettingsDescription + "\n" - + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n` + + `The user **will${notify ? '' : ' not'}** be notified\n\n` + `Are you sure you want to mute <@!${user.id}>?`) .setColor("Danger") .addCustomBoolean( - "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), - async () => await create(interaction.guild, user.user, interaction.user, reason), - "An appeal ticket will be created when Confirm is clicked") + "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), + async () => await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason), + "An appeal ticket will be created when Confirm is clicked", "CONTROL.TICKET", createAppealTicket) + .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify) .addReasonButton(reason ?? "") .send(true) reason = reason ?? "" - if (confirmation.newReason === undefined) break - reason = confirmation.newReason + if (confirmation.cancelled) return + if (confirmation.success) break + if (confirmation.newReason) reason = confirmation.newReason + if (confirmation.components) { + notify = confirmation.components.notify.active + createAppealTicket = confirmation.components.appeal.active + } } if (confirmation.success) { let dmd = false let dm; let config = await client.database.guilds.read(interaction.guild.id); try { - if (interaction.options.getString("notify") != "no") { + if (notify) { dm = await user.send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.MUTE.RED") .setTitle("Muted") .setDescription(`You have been muted in ${interaction.guild.name}` + (reason ? ` for:\n> ${reason}` : ".\n\n" + - `You will be unmuted at: at ()`)) + `You will be unmuted at: at ()`) + + (confirmation.components.appeal.response ? `You can appeal this here: <#${confirmation.components.appeal.response}>` : ``)) .setStatus("Danger") ], components: [new MessageActionRow().addComponents(config.moderation.mute.text ? [new MessageButton() @@ -198,16 +205,16 @@ const callback = async (interaction: CommandInteraction): Promise => { .setTitle(`Mute`) .setDescription("Something went wrong and the user was not muted") .setStatus("Danger") - ], components: []}) + ], components: []}) // TODO: make this clearer if (dmd) await dm.delete() return } try { await client.database.history.create("mute", interaction.guild.id, member.user, interaction.user, reason) } catch {} - let failed = (dmd == false && interaction.options.getString("notify") != "no") + let failed = (dmd == false && notify) await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`) .setTitle(`Mute`) - .setDescription("The member was muted" + (failed ? ", but could not be notified" : "")) + .setDescription("The member was muted" + (failed ? ", but could not be notified" : "") + (confirmation.components.appeal.response ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>` : ``)) .setStatus(failed ? "Warning" : "Success") ], components: []}) let data = { diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts index f842d76..3ff18ec 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -13,40 +13,43 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Changes a users nickname") .addUserOption(option => option.setName("user").setDescription("The user to change").setRequired(true)) .addStringOption(option => option.setName("name").setDescription("The name to set | Leave blank to clear").setRequired(false)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when their nickname is changed | Default: No").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]]) - ) const callback = async (interaction: CommandInteraction): Promise => { const { renderUser } = client.logger // TODO:[Modals] Replace this with a modal - let confirmation = await new confirmationMessage(interaction) - .setEmoji("PUNISH.NICKNAME.RED") - .setTitle("Nickname") - .setDescription(keyValueList({ - "user": renderUser(interaction.options.getUser("user")), - "new nickname": `${interaction.options.getString("name") ? interaction.options.getString("name") : "*No nickname*"}` - }) - + `The user **will${interaction.options.getString("notify") == "yes" ? '' : ' not'}** be notified\n\n` - + `Are you sure you want to ${interaction.options.getString("name") ? "change" : "clear"} <@!${(interaction.options.getMember("user") as GuildMember).id}>'s nickname?`) - .setColor("Danger") - .addCustomBoolean( - "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), - async () => await create(interaction.guild, interaction.options.getUser("user"), interaction.user, null), - "An appeal ticket will be created when Confirm is clicked") - .send() + let notify = true; + let confirmation; + while (true) { + confirmation = await new confirmationMessage(interaction) + .setEmoji("PUNISH.NICKNAME.RED") + .setTitle("Nickname") + .setDescription(keyValueList({ + "user": renderUser(interaction.options.getUser("user")), + "new nickname": `${interaction.options.getString("name") ? interaction.options.getString("name") : "*No nickname*"}` + }) + + `The user **will${notify ? '' : ' not'}** be notified\n\n` + + `Are you sure you want to ${interaction.options.getString("name") ? "change" : "clear"} <@!${(interaction.options.getMember("user") as GuildMember).id}>'s nickname?`) + .setColor("Danger") + .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify) + .send(interaction.options.getString("name") !== null) + if (confirmation.cancelled) return + if (confirmation.success) break + if (confirmation.components) { + notify = confirmation.components.notify.active + } + } if (confirmation.success) { let dmd = false let dm; try { - if (interaction.options.getString("notify") == "yes") { + if (notify) { dm = await (interaction.options.getMember("user") as GuildMember).send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.NICKNAME.RED") .setTitle("Nickname changed") .setDescription(`Your nickname was ${interaction.options.getString("name") ? "changed" : "cleared"} in ${interaction.guild.name}.` + (interaction.options.getString("name") ? ` it is now: ${interaction.options.getString("name")}` : "") + "\n\n" + - (confirmation.buttonClicked ? `You can appeal this here: <#${confirmation.response}>` : ``)) + (confirmation.components.appeal.response ? `You can appeal this here: <#${confirmation.components.appeal.response}>` : ``)) .setStatus("Danger") ] }) @@ -61,8 +64,7 @@ const callback = async (interaction: CommandInteraction): Promise => { try { await client.database.history.create( "nickname", interaction.guild.id, member.user, interaction.user, null, before, nickname) } catch {} - // @ts-ignore - const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger + const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger let data = { meta: { type: 'memberUpdate', @@ -94,11 +96,11 @@ const callback = async (interaction: CommandInteraction): Promise => { if (dmd) await dm.delete() return } - let failed = (dmd == false && interaction.options.getString("notify") == "yes") + let failed = (dmd == false && notify) await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.NICKNAME.${failed ? "YELLOW" : "GREEN"}`) .setTitle(`Nickname`) - .setDescription("The members nickname was changed" + (failed ? ", but was not notified" : "") + (confirmation.response ? ` and an appeal ticket was opened in <#${confirmation.response}>` : ``)) + .setDescription("The members nickname was changed" + (failed ? ", but was not notified" : "") + (confirmation.components.appeal.response ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>` : ``)) .setStatus(failed ? "Warning" : "Success") ], components: []}) } else { diff --git a/src/commands/mod/purge.ts b/src/commands/mod/purge.ts index fd8e6b8..af7beb3 100644 --- a/src/commands/mod/purge.ts +++ b/src/commands/mod/purge.ts @@ -132,7 +132,6 @@ const callback = async (interaction: CommandInteraction): Promise => { } let attachmentObject; try { - // @ts-ignore const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger let data = { meta: { @@ -210,6 +209,7 @@ const callback = async (interaction: CommandInteraction): Promise => { })) .setColor("Danger") .send() + if (confirmation.cancelled) return if (confirmation.success) { let messages; try { @@ -234,7 +234,6 @@ const callback = async (interaction: CommandInteraction): Promise => { } let attachmentObject; try { - // @ts-ignore const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger let data = { meta: { diff --git a/src/commands/mod/slowmode.ts b/src/commands/mod/slowmode.ts index d9a8421..2b386fd 100644 --- a/src/commands/mod/slowmode.ts +++ b/src/commands/mod/slowmode.ts @@ -31,6 +31,7 @@ const callback = async (interaction: CommandInteraction): Promise => { + `Are you sure you want to set the slowmode in this channel?`) .setColor("Danger") .send() + if (confirmation.cancelled) return if (confirmation.success) { try { (interaction.channel as TextChannel).setRateLimitPerUser(time) diff --git a/src/commands/mod/softban.ts b/src/commands/mod/softban.ts index a368e7a..7fefb1b 100644 --- a/src/commands/mod/softban.ts +++ b/src/commands/mod/softban.ts @@ -13,14 +13,12 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setDescription("Kicks a user and deletes their messages") .addUserOption(option => option.setName("user").setDescription("The user to softban").setRequired(true)) .addIntegerOption(option => option.setName("delete").setDescription("The days of messages to delete | Default: 0").setMinValue(0).setMaxValue(7).setRequired(false)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are softbanned | Default: Yes").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]]) - ) const callback = async (interaction: CommandInteraction): Promise => { const { renderUser } = client.logger // TODO:[Modals] Replace this with a modal let reason = null; + let notify = true; let confirmation; while (true) { let confirmation = await new confirmationMessage(interaction) @@ -30,21 +28,26 @@ const callback = async (interaction: CommandInteraction): Promise => { "user": renderUser(interaction.options.getUser("user")), "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*" }) - + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n` + + `The user **will${notify ? '' : ' not'}** be notified\n` + `${addPlural(interaction.options.getInteger("delete") ? interaction.options.getInteger("delete") : 0, "day")} of messages will be deleted\n\n` + `Are you sure you want to softban <@!${(interaction.options.getMember("user") as GuildMember).id}>?`) .setColor("Danger") + .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify) .addReasonButton(reason ?? "") .send(reason !== null) reason = reason ?? "" - if (confirmation.newReason === undefined) break - reason = confirmation.newReason + if (confirmation.cancelled) return + if (confirmation.success) break + if (confirmation.newReason) reason = confirmation.newReason + if (confirmation.components) { + notify = confirmation.components.notify.active + } } if (confirmation.success) { let dmd = false; let config = await client.database.guilds.read(interaction.guild.id); try { - if (interaction.options.getString("notify") != "no") { + if (notify) { await (interaction.options.getMember("user") as GuildMember).send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.BAN.RED") @@ -78,7 +81,7 @@ const callback = async (interaction: CommandInteraction): Promise => { ], components: []}) } try { await client.database.history.create("softban", interaction.guild.id, member.user, reason) } catch {} - let failed = (dmd == false && interaction.options.getString("notify") != "no") + let failed = (dmd == false && notify) await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.BAN.${failed ? "YELLOW" : "GREEN"}`) .setTitle(`Softban`) diff --git a/src/commands/mod/unban.ts b/src/commands/mod/unban.ts index e512084..035b809 100644 --- a/src/commands/mod/unban.ts +++ b/src/commands/mod/unban.ts @@ -36,12 +36,12 @@ const callback = async (interaction: CommandInteraction): Promise => { + `Are you sure you want to unban <@${resolved.user.id}>?`) .setColor("Danger") .send() + if (confirmation.cancelled) return if (confirmation.success) { try { await interaction.guild.members.unban(resolved.user as User, "Unban"); let member = (resolved.user as User) try { await client.database.history.create("unban", interaction.guild.id, member, interaction.user) } catch {} - // @ts-ignore const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger let data = { meta: { diff --git a/src/commands/mod/unmute.ts b/src/commands/mod/unmute.ts index d5f4205..56a0b56 100644 --- a/src/commands/mod/unmute.ts +++ b/src/commands/mod/unmute.ts @@ -11,14 +11,12 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("unmute") .setDescription("Unmutes a user") .addUserOption(option => option.setName("user").setDescription("The user to unmute").setRequired(true)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are unmuted | Default: No").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]]) - ) const callback = async (interaction: CommandInteraction): Promise => { const { log, NucleusColors, renderUser, entry, renderDelta } = client.logger // TODO:[Modals] Replace this with a modal let reason = null; + let notify = false; let confirmation; while (true) { confirmation = await new confirmationMessage(interaction) @@ -28,20 +26,23 @@ const callback = async (interaction: CommandInteraction): Promise => { "user": renderUser(interaction.options.getUser("user")), "reason": `\n> ${reason ? reason : "*No reason provided*"}` }) - + `The user **will${interaction.options.getString("notify") === "yes" ? '' : ' not'}** be notified\n\n` + + `The user **will${notify ? '' : ' not'}** be notified\n\n` + `Are you sure you want to unmute <@!${(interaction.options.getMember("user") as GuildMember).id}>?`) .setColor("Danger") .addReasonButton(reason ?? "") .send(reason !== null) - reason = reason ?? "" - if (confirmation.newReason === undefined) break - reason = confirmation.newReason + if (confirmation.success) break + if (confirmation.newReason) reason = confirmation.newReason + if (confirmation.components) { + notify = confirmation.components.notify.active + } } + if (confirmation.cancelled) return if (confirmation.success) { let dmd = false let dm; try { - if (interaction.options.getString("notify") != "no") { + if (notify) { dm = await (interaction.options.getMember("user") as GuildMember).send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.MUTE.GREEN") @@ -88,7 +89,7 @@ const callback = async (interaction: CommandInteraction): Promise => { } } log(data); - let failed = (dmd == false && interaction.options.getString("notify") != "no") + let failed = (dmd == false && notify) await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.MUTE.${failed ? "YELLOW" : "GREEN"}`) .setTitle(`Unmute`) diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts index 379d49c..3e76321 100644 --- a/src/commands/mod/warn.ts +++ b/src/commands/mod/warn.ts @@ -1,4 +1,4 @@ -import Discord, { CommandInteraction, GuildMember, MessageActionRow } from "discord.js"; +import Discord, { CommandInteraction, GuildMember, MessageActionRow, MessageButton } from "discord.js"; import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import { WrappedCheck } from "jshaiku"; import confirmationMessage from "../../utils/confirmationMessage.js"; @@ -12,14 +12,13 @@ const command = (builder: SlashCommandSubcommandBuilder) => .setName("warn") .setDescription("Warns a user") .addUserOption(option => option.setName("user").setDescription("The user to warn").setRequired(true)) - .addStringOption(option => option.setName("notify").setDescription("If the user should get a message when they are warned | Default: Yes").setRequired(false) - .addChoices([["Yes", "yes"], ["No", "no"]]) - ) const callback = async (interaction: CommandInteraction): Promise => { const { log, NucleusColors, renderUser, entry } = client.logger // TODO:[Modals] Replace this with a modal let reason = null; + let notify = true; + let createAppealTicket = false; let confirmation; while (true) { confirmation = await new confirmationMessage(interaction) @@ -29,44 +28,52 @@ const callback = async (interaction: CommandInteraction): Promise => { "user": renderUser(interaction.options.getUser("user")), "reason": reason ? ("\n> " + ((reason ?? "").replaceAll("\n", "\n> "))) : "*No reason provided*" }) - + `The user **will${interaction.options.getString("notify") === "no" ? ' not' : ''}** be notified\n\n` + + `The user **will${notify ? '' : ' not'}** be notified\n\n` + `Are you sure you want to warn <@!${(interaction.options.getMember("user") as GuildMember).id}>?`) .setColor("Danger") .addCustomBoolean( - "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), + "appeal", "Create appeal ticket", !(await areTicketsEnabled(interaction.guild.id)), async () => await create(interaction.guild, interaction.options.getUser("user"), interaction.user, reason), - "An appeal ticket will be created when Confirm is clicked") - .addReasonButton(reason) + "An appeal ticket will be created when Confirm is clicked", "CONTROL.TICKET", createAppealTicket) + .addCustomBoolean("notify", "Notify user", false, null, null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF" ), notify) .addReasonButton(reason ?? "") .send(reason !== null) reason = reason ?? "" - if (confirmation.newReason === undefined) break - reason = confirmation.newReason + if (confirmation.cancelled) return + if (confirmation.success) break + if (confirmation.newReason) reason = confirmation.newReason + if (confirmation.components) { + notify = confirmation.components.notify.active + createAppealTicket = confirmation.components.appeal.active + } } if (confirmation.success) { let dmd = false try { - if (interaction.options.getString("notify") != "no") { + if (notify) { + const config = await client.database.guilds.read(interaction.guild.id) await (interaction.options.getMember("user") as GuildMember).send({ embeds: [new EmojiEmbed() .setEmoji("PUNISH.WARN.RED") .setTitle("Warned") .setDescription(`You have been warned in ${interaction.guild.name}` + (reason ? ` for:\n> ${reason}` : ".") + "\n\n" + - (confirmation.buttonClicked ? `You can appeal this here ticket: <#${confirmation.response}>` : ``)) + (confirmation.components.appeal.response ? `You can appeal this here ticket: <#${confirmation.components.appeal.response}>` : ``)) .setStatus("Danger") - ] + .setFooter({ + text: config.moderation.warn.text ? "The button below is set by the server admins. Do not enter any passwords or other account details on the linked site." : "", + iconURL: "https://cdn.discordapp.com/emojis/952295894370369587.webp?size=128&quality=lossless" + }) + ], + components: config.moderation.warn.text ? [new MessageActionRow().addComponents([new MessageButton() + .setStyle("LINK") + .setLabel(config.moderation.warn.text) + .setURL(config.moderation.warn.link) + ])] : [] }) dmd = true } - } catch { - await interaction.editReply({embeds: [new EmojiEmbed() - .setEmoji("PUNISH.WARN.RED") - .setTitle(`Warn`) - .setDescription("Something went wrong and the user was not warned") - .setStatus("Danger") - ], components: []}) - } + } catch {} let data = { meta:{ type: 'memberWarn', @@ -91,12 +98,12 @@ const callback = async (interaction: CommandInteraction): Promise => { interaction.user, reason )} catch {} log(data); - let failed = (dmd == false && interaction.options.getString("notify") != "no") + let failed = (dmd == false && notify) if (!failed) { await interaction.editReply({embeds: [new EmojiEmbed() .setEmoji(`PUNISH.WARN.GREEN`) .setTitle(`Warn`) - .setDescription("The user was warned" + (confirmation.response ? ` and an appeal ticket was opened in <#${confirmation.response}>` : ``)) + .setDescription("The user was warned" + (confirmation.components.appeal.response ? ` and an appeal ticket was opened in <#${confirmation.components.appeal.response}>` : ``)) .setStatus("Success") ], components: []}) } else { @@ -118,7 +125,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStyle("SECONDARY") .setDisabled((interaction.options.getMember("user") as GuildMember).permissionsIn(interaction.channel as Discord.TextChannel).has("VIEW_CHANNEL") === false), ]) - ], + ] }) let component; try { diff --git a/src/commands/nucleus/suggest.ts b/src/commands/nucleus/suggest.ts index 4e3a1c8..0c596ff 100644 --- a/src/commands/nucleus/suggest.ts +++ b/src/commands/nucleus/suggest.ts @@ -12,7 +12,6 @@ const command = (builder: SlashCommandSubcommandBuilder) => .addStringOption(option => option.setName("suggestion").setDescription("The suggestion to send").setRequired(true)) const callback = async (interaction: CommandInteraction): Promise => { - // @ts-ignore const { renderUser } = client.logger let suggestion = interaction.options.getString("suggestion"); let confirmation = await new confirmationMessage(interaction) @@ -23,6 +22,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Danger") .setInverted(true) .send() + if (confirmation.cancelled) return if (confirmation.success) { await (client.channels.cache.get('955161206459600976') as Discord.TextChannel).send({ embeds: [ diff --git a/src/commands/role/user.ts b/src/commands/role/user.ts index c2ade39..c431d39 100644 --- a/src/commands/role/user.ts +++ b/src/commands/role/user.ts @@ -28,10 +28,10 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription(keyValueList({ "user": renderUser(interaction.options.getUser("user")), "role": renderRole(interaction.options.getRole("role")) - }) - + `\nAre you sure you want to ${action == "give" ? "give the role to" : "remove the role from"} ${interaction.options.getUser("user")}?`) + }) + `\nAre you sure you want to ${action == "give" ? "give the role to" : "remove the role from"} ${interaction.options.getUser("user")}?`) .setColor("Danger") .send() + if (confirmation.cancelled) return if (confirmation.success) { try { let member = interaction.options.getMember("user") as GuildMember diff --git a/src/commands/settings/logs/channel.ts b/src/commands/settings/logs/channel.ts index 10a5887..aacc2e7 100644 --- a/src/commands/settings/logs/channel.ts +++ b/src/commands/settings/logs/channel.ts @@ -50,6 +50,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Warning") .setInverted(true) .send(true) + if (confirmation.cancelled) return if (confirmation.success) { try { await client.database.guilds.write(interaction.guild.id, {"logging.logs.channel": channel.id}) diff --git a/src/commands/settings/logs/ignore.ts b/src/commands/settings/logs/ignore.ts index 3b81d42..59f6621 100644 --- a/src/commands/settings/logs/ignore.ts +++ b/src/commands/settings/logs/ignore.ts @@ -96,6 +96,7 @@ const callback = async (interaction: CommandInteraction): Promise => { + `Are you sure you want to **${interaction.options.getString("action") == "add" ? "add" : "remove"}** these to the ignore list?`) .setColor("Warning") .send(true) + if (confirmation.cancelled) return if (confirmation.success) { let data = client.database.guilds.read(interaction.guild.id) if (channel) data.logging.logs.ignore.channels.concat([channel.id]) diff --git a/src/commands/settings/staff.ts b/src/commands/settings/staff.ts deleted file mode 100644 index e0d2776..0000000 --- a/src/commands/settings/staff.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { ChannelType } from 'discord-api-types'; -import Discord, { CommandInteraction, MessageActionRow, MessageButton } from "discord.js"; -import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; -import confirmationMessage from "../../utils/confirmationMessage.js"; -import getEmojiByName from "../../utils/getEmojiByName.js"; -import { SlashCommandSubcommandBuilder } from "@discordjs/builders"; -import { WrappedCheck } from "jshaiku"; -import client from "../../utils/client.js"; - -const command = (builder: SlashCommandSubcommandBuilder) => - builder - .setName("staff") - .setDescription("Settings for the staff notifications channel") - .addChannelOption(option => option.setName("channel").setDescription("The channel to set the staff notifications channel to").addChannelTypes([ - ChannelType.GuildNews, ChannelType.GuildText - ]).setRequired(false)) - -const callback = async (interaction: CommandInteraction): Promise => { - let m; - m = await interaction.reply({embeds: [new EmojiEmbed() - .setTitle("Loading") - .setStatus("Danger") - .setEmoji("NUCLEUS.LOADING") - ], ephemeral: true, fetchReply: true}); - if (interaction.options.getChannel("channel")) { - let channel - try { - channel = interaction.options.getChannel("channel") - } catch { - return await interaction.editReply({embeds: [new EmojiEmbed() - .setEmoji("CHANNEL.TEXT.DELETE") - .setTitle("Staff Notifications Channel") - .setDescription("The channel you provided is not a valid channel") - .setStatus("Danger") - ]}) - } - channel = channel as Discord.TextChannel - if (channel.guild.id != interaction.guild.id) { - return interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Staff Notifications Channel") - .setDescription(`You must choose a channel in this server`) - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ]}); - } - let confirmation = await new confirmationMessage(interaction) - .setEmoji("CHANNEL.TEXT.EDIT") - .setTitle("Staff Notifications Channel") - .setDescription( - `This will be the channel all notifications, updates, user reports etc. will be sent to.\n\n` + - `Are you sure you want to set the staff notifications channel to <#${channel.id}>?` - ) - .setColor("Warning") - .setInverted(true) - .send(true) - if (confirmation.success) { - try { - await client.database.guilds.write(interaction.guild.id, {"logging.staff.channel": channel.id}) - const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger - try { - let data = { - meta:{ - type: 'logIgnoreUpdated', - displayName: 'Staff Notifications Channel Updated', - calculateType: 'nucleusSettingsUpdated', - color: NucleusColors.yellow, - emoji: "CHANNEL.TEXT.EDIT", - timestamp: new Date().getTime() - }, - list: { - memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), - changedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(channel.id, renderChannel(channel)), - }, - hidden: { - guild: interaction.guild.id - } - } - log(data); - } catch {} - } catch (e) { - return interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Staff Notifications Channel") - .setDescription(`Something went wrong and the staff notifications channel could not be set`) - .setStatus("Danger") - .setEmoji("CHANNEL.TEXT.DELETE") - ], components: []}); - } - } else { - return interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Staff Notifications Channel") - .setDescription(`No changes were made`) - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], components: []}); - } - } - let clicks = 0; - let data = await client.database.guilds.read(interaction.guild.id); - let channel = data.logging.staff.channel; - while (true) { - await interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Staff Notifications channel") - .setDescription(channel ? `Your staff notifications channel is currently set to <#${channel}>` : "This server does not have a staff notifications channel") - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - ], components: [new MessageActionRow().addComponents([new MessageButton() - .setCustomId("clear") - .setLabel(clicks ? "Click again to confirm" : "Reset channel") - .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id")) - .setStyle("DANGER") - .setDisabled(!channel) - ])]}); - let i; - try { - i = await m.awaitMessageComponent({time: 300000}); - } catch(e) { break } - i.deferUpdate() - if (i.component.customId == "clear") { - clicks += 1; - if (clicks == 2) { - clicks = 0; - await client.database.guilds.write(interaction.guild.id, {}, ["logging.staff.channel"]) - channel = undefined; - } - } else { - break - } - } - await interaction.editReply({embeds: [new EmojiEmbed() - .setTitle("Staff Notifications channel") - .setDescription(channel ? `Your staff notifications channel is currently set to <#${channel}>` : "This server does not have a staff notifications channel") - .setStatus("Success") - .setEmoji("CHANNEL.TEXT.CREATE") - .setFooter({text: "Message closed"}) - ], components: [new MessageActionRow().addComponents([new MessageButton() - .setCustomId("clear") - .setLabel("Clear") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - .setStyle("SECONDARY") - .setDisabled(true) - ])]}); -} - -const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { - let member = (interaction.member as Discord.GuildMember) - if (!member.permissions.has("MANAGE_GUILD")) throw "You must have the Manage Server permission to use this command" - return true; -} - -export { command }; -export { callback }; -export { check }; diff --git a/src/commands/settings/tickets.ts b/src/commands/settings/tickets.ts index ba16751..16d5f3b 100644 --- a/src/commands/settings/tickets.ts +++ b/src/commands/settings/tickets.ts @@ -109,6 +109,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Warning") .setInverted(true) .send(true) + if (confirmation.cancelled) return if (confirmation.success) { let toUpdate = {} if (options.enabled !== null) toUpdate["tickets.enabled"] = options.enabled @@ -345,8 +346,7 @@ async function manageTypes(interaction, data, m) { } } else if (i.component.customId == "addType") { await i.showModal(new Discord.Modal().setCustomId("modal").setTitle("Enter a name for the new type").addComponents( - // @ts-ignore - new MessageActionRow().addComponents(new TextInputComponent() + new MessageActionRow().addComponents(new TextInputComponent() .setCustomId("type") .setLabel("Name") .setMaxLength(100) diff --git a/src/commands/settings/verify.ts b/src/commands/settings/verify.ts index 7a68c64..d71fdf0 100644 --- a/src/commands/settings/verify.ts +++ b/src/commands/settings/verify.ts @@ -47,6 +47,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Warning") .setInverted(true) .send(true) + if (confirmation.cancelled) return if (confirmation.success) { try { await client.database.guilds.write(interaction.guild.id, {"verify.role": role.id, "verify.enabled": true}); diff --git a/src/commands/tag.ts b/src/commands/tag.ts index d032598..0c44e37 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -1,21 +1,58 @@ -import { CommandInteraction } from "discord.js"; +import { AutocompleteInteraction, CommandInteraction, MessageActionRow, MessageButton } from "discord.js"; import { SlashCommandBuilder } from "@discordjs/builders"; import { WrappedCheck } from "jshaiku"; -import { callback as statsChannelAdd } from '../reflex/statsChannelAdd.js'; import client from "../utils/client.js" +import EmojiEmbed from "../utils/generateEmojiEmbed.js"; const command = new SlashCommandBuilder() .setName("tag") .setDescription("Get and manage the servers tags") + .addStringOption(o => o.setName("tag").setDescription("The tag to get").setAutocomplete(true).setRequired(true)) + +const callback = async (interaction: CommandInteraction) => { + const config = await client.database.guilds.read(interaction.guild.id) + const tags = config.getKey("tags") + const tag = tags[interaction.options.getString("tag")] + if (!tag) { + return await interaction.reply({embeds: [new EmojiEmbed() + .setTitle("Tag") + .setDescription(`Tag \`${interaction.options.getString("tag")}\` does not exist`) + .setEmoji("PUNISH.NICKNAME.RED") + .setStatus("Danger") + ], ephemeral: true}) + } + let url = "" + let components = [] + if (tag.match(/^(http|https):\/\/[^ "]+$/)) { + url = tag + components = [new MessageActionRow().addComponents([new MessageButton() + .setLabel("Open") + .setURL(url) + .setStyle("LINK") + ])] + } + return await interaction.reply({embeds: [new EmojiEmbed() + .setTitle(interaction.options.getString("tag")) + .setDescription(tag) + .setEmoji("PUNISH.NICKNAME.GREEN") + .setStatus("Success") + .setImage(url) + ], components: components, ephemeral: true}) -const callback = (interaction: CommandInteraction) => { - interaction.reply("This command is not yet finished [tag]"); } const check = (interaction: CommandInteraction, defaultCheck: WrappedCheck) => { return true; } +const autocomplete = async (interaction: AutocompleteInteraction): Promise => { + if (!interaction.guild) return []; + const config = await client.database.guilds.read(interaction.guild.id) + const tags = Object.keys(config.getKey("tags")); + return tags +} + export { command }; export { callback }; export { check }; +export { autocomplete }; \ No newline at end of file diff --git a/src/commands/tags/create.ts b/src/commands/tags/create.ts index 6dbecf4..0e0d54d 100644 --- a/src/commands/tags/create.ts +++ b/src/commands/tags/create.ts @@ -52,6 +52,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Warning") .setInverted(true) .send() + if (confirmation.cancelled) return if (!confirmation) return await interaction.editReply({embeds: [new EmojiEmbed() .setTitle("Tag Create") .setDescription("No changes were made") diff --git a/src/commands/tags/delete.ts b/src/commands/tags/delete.ts index 2707465..74ebb25 100644 --- a/src/commands/tags/delete.ts +++ b/src/commands/tags/delete.ts @@ -32,6 +32,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Warning") .setInverted(true) .send() + if (confirmation.cancelled) return if (!confirmation) return await interaction.editReply({embeds: [new EmojiEmbed() .setTitle("Tag Delete") .setDescription("No changes were made") @@ -39,9 +40,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setEmoji("PUNISH.NICKNAME.GREEN") ]}); try { - data = await client.database.guilds.read(interaction.guild.id); - delete data.tags[name]; - await client.database.guilds.write(interaction.guild.id, {tags: data}); + await client.database.guilds.write(interaction.guild.id, null, [`tags.${name}`]); } catch (e) { return await interaction.editReply({embeds: [new EmojiEmbed() .setTitle("Tag Delete") diff --git a/src/commands/tags/edit.ts b/src/commands/tags/edit.ts index 77e21ae..2ecdfbf 100644 --- a/src/commands/tags/edit.ts +++ b/src/commands/tags/edit.ts @@ -60,6 +60,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setColor("Warning") .setInverted(true) .send() + if (confirmation.cancelled) return if (!confirmation) return await interaction.editReply({embeds: [new EmojiEmbed() .setTitle("Tag Edit") .setDescription("No changes were made") diff --git a/src/config/default.json b/src/config/default.json index 81c7880..2f95e94 100644 --- a/src/config/default.json +++ b/src/config/default.json @@ -52,7 +52,7 @@ "channel": null, "message": null }, - "stats": [], + "stats": {}, "logging": { "logs": { "enabled": true, diff --git a/src/config/emojis.json b/src/config/emojis.json index 2f32df1..7024a73 100644 --- a/src/config/emojis.json +++ b/src/config/emojis.json @@ -22,6 +22,10 @@ "FILTER": "990242059451514902", "ATTACHMENT": "997570687193587812", "LOGGING": "999613304446144562", + "NOTIFY": { + "ON": "1000726394579464232", + "OFF": "1000726363495477368" + }, "OPP": { "ADD": "837355918831124500", "REMOVE": "837355918420869162" diff --git a/src/events/guildBanAdd.ts b/src/events/guildBanAdd.ts index 64a1604..d095049 100644 --- a/src/events/guildBanAdd.ts +++ b/src/events/guildBanAdd.ts @@ -1,5 +1,5 @@ import { purgeByUser } from '../actions/tickets/delete.js'; -import { callback as statsChannelRemove } from '../reflex/statsChannelRemove.js'; +import { callback as statsChannelRemove } from '../reflex/statsChannelUpdate.js'; export const event = 'guildBanAdd'; diff --git a/src/events/guildBanRemove.ts b/src/events/guildBanRemove.ts index a6d1c14..4d6d1f8 100644 --- a/src/events/guildBanRemove.ts +++ b/src/events/guildBanRemove.ts @@ -1,6 +1,6 @@ import humanizeDuration from 'humanize-duration'; import { purgeByUser } from '../actions/tickets/delete.js'; -import { callback as statsChannelRemove } from '../reflex/statsChannelRemove.js'; +import { callback as statsChannelRemove } from '../reflex/statsChannelUpdate.js'; export const event = 'guildBanRemove'; diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 1fd1e1f..900c774 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -3,18 +3,43 @@ import verify from "../reflex/verify.js"; import create from "../actions/tickets/create.js"; import close from "../actions/tickets/delete.js"; import createTranscript from "../premium/createTranscript.js"; +import Fuse from "fuse.js"; +import { autocomplete as tagAutocomplete } from "../commands/tag.js" export const event = 'interactionCreate'; + +function getAutocomplete(typed: string, options: string[]): object[] { + options = options.filter(option => option.length <= 100) // thanks discord. 6000 character limit on slash command inputs but only 100 for autocomplete. + if (!typed) return options.slice(0, 25).sort().map(option => ({name: option, value: option})) + const fuse = new Fuse(options, {useExtendedSearch: true, findAllMatches: true, minMatchCharLength: 0}).search(typed) + return fuse.slice(0, 25).map(option => ({name: option.item, value: option.item})) +} + +const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"] +function generateStatsChannelAutocomplete(typed) { + let autocompletions = [] + const beforeLastOpenBracket = typed.match(/(.*){[^{}]{0,15}$/) + if (beforeLastOpenBracket !== null) { for (let replacement of validReplacements) { autocompletions.push(`${beforeLastOpenBracket[1]} {${replacement}}`) } } + else { for (let replacement of validReplacements) { autocompletions.push(`${typed} {${replacement}}`) } } + return getAutocomplete(typed, autocompletions) +} + async function interactionCreate(interaction) { if (interaction.componentType === "BUTTON") { - if (interaction.customId === "rolemenu") return await roleMenu(interaction) - if (interaction.customId === "verifybutton") return verify(interaction) - if (interaction.customId === "createticket") return create(interaction) - if (interaction.customId === "closeticket") return close(interaction) - if (interaction.customId === "createtranscript") return createTranscript(interaction) + switch (interaction.customId) { + case "rolemenu": { return await roleMenu(interaction) } + case "verifybutton": { return verify(interaction) } + case "createticket": { return create(interaction) } + case "closeticket": { return close(interaction) } + case "createtranscript": { return createTranscript(interaction) } + } } else if (interaction.componentType === "MESSAGE_COMPONENT") { - console.table(interaction) + } else if (interaction.type === "APPLICATION_COMMAND_AUTOCOMPLETE") { + switch (`${interaction.commandName} ${interaction.options.getSubcommandGroup(false)} ${interaction.options.getSubcommand(false)}`) { + case `tag null null`: { return interaction.respond(getAutocomplete(interaction.options.getString("tag"), (await tagAutocomplete(interaction)))) } + case `settings stats set`: { return interaction.respond(generateStatsChannelAutocomplete(interaction.options.getString("name"))) } + } } } diff --git a/src/events/memberJoin.ts b/src/events/memberJoin.ts index b1c2700..7bee084 100644 --- a/src/events/memberJoin.ts +++ b/src/events/memberJoin.ts @@ -1,4 +1,4 @@ -import { callback as statsChannelAdd } from '../reflex/statsChannelAdd.js'; +import { callback as statsChannelAdd } from '../reflex/statsChannelUpdate.js'; import { callback as welcome } from '../reflex/welcome.js'; import log from '../utils/log.js'; import client from '../utils/client.js'; @@ -7,7 +7,6 @@ export const event = 'guildMemberAdd' export async function callback(_, member) { try { welcome(_, member); } catch {} - try { statsChannelAdd(_, member); } catch {} try { const { log, NucleusColors, entry, renderUser, renderDelta } = member.client.logger try { await client.database.history.create("join", member.guild.id, member.user, null, null) } catch {} @@ -33,4 +32,5 @@ export async function callback(_, member) { } log(data); } catch {} + try { statsChannelAdd(_, member, ); } catch {} } diff --git a/src/events/memberLeave.ts b/src/events/memberLeave.ts index 592a630..122e01a 100644 --- a/src/events/memberLeave.ts +++ b/src/events/memberLeave.ts @@ -1,11 +1,10 @@ import humanizeDuration from 'humanize-duration'; import { purgeByUser } from '../actions/tickets/delete.js'; -import { callback as statsChannelRemove } from '../reflex/statsChannelRemove.js'; +import { callback as statsChannelRemove } from '../reflex/statsChannelUpdate.js'; export const event = 'guildMemberRemove' export async function callback(client, member) { - try { await statsChannelRemove(client, member); } catch {} try { purgeByUser(member.id, member.guild); } catch {} try { const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = member.client.logger @@ -72,4 +71,5 @@ export async function callback(client, member) { } log(data); } catch (e) { console.log(e) } + try { await statsChannelRemove(client, member); } catch {} } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 7db7d39..dce1959 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,7 +1,6 @@ import { LinkCheck, MalwareCheck, NSFWCheck, SizeCheck, TestString, TestImage } from '../reflex/scanners.js' import logAttachment from '../premium/attachmentLogs.js' import createLogException from '../utils/createLogException.js' -import { capitalize } from '../utils/generateKeyValueList.js' import getEmojiByName from '../utils/getEmojiByName.js' export const event = 'messageCreate' @@ -19,7 +18,7 @@ export async function callback(client, message) { let config = await client.memory.readGuildInfo(message.guild.id); const filter = getEmojiByName("ICONS.FILTER") let attachmentJump = "" - if (config.logging.attachments.saved[message.channel.id + message.id]) { attachmentJump = ` [[View attachments]](${config})` } + if (config.logging.attachments.saved[message.channel.id + message.id]) { attachmentJump = ` [[View attachments]](${config.logging.attachments.saved[message.channel.id + message.id]})` } let list = { messageId: entry(message.id, `\`${message.id}\``), sentBy: entry(message.author.id, renderUser(message.author)), @@ -64,7 +63,7 @@ export async function callback(client, message) { } if (fileNames.files.length > 0) { - fileNames.files.forEach(async element => { + for (let element of fileNames.files) { if(!message) return; let url = element.url ? element.url : element.local if (url != undefined) { @@ -173,7 +172,7 @@ export async function callback(client, message) { } } } - }); + }; } if(!message) return; diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts index 2263d51..3c23739 100644 --- a/src/events/messageDelete.ts +++ b/src/events/messageDelete.ts @@ -14,6 +14,7 @@ export async function callback(client, message) { let content = message.cleanContent content.replace(`\``, `\\\``) if (content.length > 256) content = content.substring(0, 253) + '...' + let attachments = message.attachments.size + (message.content.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi) ?? []).length let attachmentJump = "" let config = (await client.database.guilds.read(message.guild.id)).logging.attachments.saved[message.channel.id + message.id]; if (config) { attachmentJump = ` [[View attachments]](${config})` } @@ -35,7 +36,7 @@ export async function callback(client, message) { sentIn: entry(message.channel.id, renderChannel(message.channel)), deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())), mentions: message.mentions.users.size, - attachments: entry(message.attachments.size, message.attachments.size + attachmentJump), + attachments: entry(attachments, attachments + attachmentJump), repliedTo: entry( message.reference.messageId || null, message.reference.messageId ? `[[Jump to message]](https://discord.com/channels/${message.guild.id}/${message.channel.id}/${message.reference.messageId})` : "None" diff --git a/src/index.ts b/src/index.ts index 0ae917c..5f5987e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,9 @@ await client.registerEventsIn("./events"); client.on("ready", () => { runServer(client); }); +process.on("unhandledRejection", (err) => { + console.error(err); +}); client.logger = new Logger() client.verify = {} diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts index 8ea22f7..a1a5974 100644 --- a/src/reflex/scanners.ts +++ b/src/reflex/scanners.ts @@ -4,14 +4,16 @@ import { writeFileSync } from 'fs' import generateFileName from '../utils/temp/generateFileName.js' import Tesseract from 'node-tesseract-ocr'; +interface NSFWSchema { nsfw: boolean } +interface MalwareSchema { safe: boolean } -export async function testNSFW(link: string): Promise { +export async function testNSFW(link: string): Promise { let p = await saveAttachment(link) let result = await us.nsfw.file(p) return result } -export async function testMalware(link: string): Promise { +export async function testMalware(link: string): Promise { let p = await saveAttachment(link) let result = await us.malware.file(p) return result @@ -24,7 +26,7 @@ export async function saveAttachment(link): Promise { return fileName } -export async function testLink(link: string): Promise { +export async function testLink(link: string): Promise { return await us.link.scan(link) } @@ -75,7 +77,6 @@ export async function LinkCheck(message): Promise { export async function NSFWCheck(element): Promise { try { let test = (await testNSFW(element)) - //@ts-ignore return test.nsfw } catch { return false @@ -90,8 +91,7 @@ export async function SizeCheck(element): Promise { export async function MalwareCheck(element): Promise { try { - //@ts-ignore - return (await scan.testMalware(element)).safe + return (await testMalware(element)).safe } catch { return true } diff --git a/src/reflex/statsChannelAdd.ts b/src/reflex/statsChannelAdd.ts deleted file mode 100644 index 32de0ff..0000000 --- a/src/reflex/statsChannelAdd.ts +++ /dev/null @@ -1,33 +0,0 @@ -import convertCurlyBracketString from '../utils/convertCurlyBracketString.js' -import singleNotify from '../utils/singleNotify.js'; -import client from '../utils/client.js'; - -export async function callback(_, member) { - let config = await client.database.guilds.read(member.guild.id); - - config.stats.forEach(async element => { - if (element.enabled) { - let string = element.text - if (!string) return - string = await convertCurlyBracketString(string, member.id, member.displayName, member.guild.name, member.guild.members) - let channel; - try { - channel = await member.client.channels.fetch(element.channel) - } catch (error) { channel = null } - if (!channel) { - return singleNotify( - "statsChannelDeleted", - member.guild.id, - "One or more of your stats channels have been deleted. Please open the settings menu to change this.", - "Critical" - ) - } - if (channel.guild.id !== member.guild.id) return - try { - await channel.edit({ name: string }) - } catch (err) { - console.error(err) - } - } - }); -} diff --git a/src/reflex/statsChannelRemove.ts b/src/reflex/statsChannelRemove.ts deleted file mode 100644 index c6d4e65..0000000 --- a/src/reflex/statsChannelRemove.ts +++ /dev/null @@ -1,30 +0,0 @@ -import client from '../utils/client.js'; -import convertCurlyBracketString from '../utils/convertCurlyBracketString.js' -import singleNotify from '../utils/singleNotify.js'; - -export async function callback(_, member) { - let config = await client.database.guilds.read(member.guild.id); - - config.stats.forEach(async element => { - if (element.enabled) { - let string = element.text - if (!string) return - string = await convertCurlyBracketString(string, member.id, member.displayName, member.guild.name, member.guild.members) - let channel; - try { - channel = await member.client.channels.fetch(element.channel) - } catch { channel = null } - if (!channel) return singleNotify( - "statsChannelDeleted", - member.guild.id, - "One or more of your stats channels have been deleted. Please open the settings menu to change this.", - "Critical" - ) - try { - await channel.edit({ name: string }) - } catch (err) { - console.error(err) - } - } - }); -} \ No newline at end of file diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts index da10cfb..988fd37 100644 --- a/src/utils/confirmationMessage.ts +++ b/src/utils/confirmationMessage.ts @@ -3,20 +3,24 @@ import { modalInteractionCollector } from "./dualCollector.js"; import EmojiEmbed from "./generateEmojiEmbed.js" import getEmojiByName from "./getEmojiByName.js"; + +interface CustomBoolean { + title: string; + disabled: boolean; + value: string | null; + emoji: string | null; + active: boolean; + onClick: () => Promise; + response: T | null; +} + class confirmationMessage { interaction: CommandInteraction; title: string = ""; emoji: string = ""; description: string = ""; color: string = ""; - customCallback: () => any = () => {}; - customButtonTitle: string; - customButtonDisabled: boolean; - customCallbackString: string = ""; - customCallbackClicked: boolean = false; - customCallbackResponse: any = null; - customBoolean: () => any = () => {}; // allow multiple booleans - customBooleanClicked: boolean = null; + customButtons: {[index:string]: CustomBoolean} = {}; inverted: boolean = false; reason: string | null = null; @@ -29,21 +33,15 @@ class confirmationMessage { setDescription(description: string) { this.description = description; return this } setColor(color: string) { this.color = color; return this } setInverted(inverted: boolean) { this.inverted = inverted; return this } - addCustomCallback(title: string, disabled: boolean, callback: () => any, callbackClicked: string) { - if (this.customButtonTitle) return this - this.customButtonTitle = title; - this.customButtonDisabled = disabled; - this.customCallback = callback; - this.customCallbackString = callbackClicked; - return this; - } - addCustomBoolean(title: string, disabled: boolean, callback: () => any, callbackClicked: string) { - if (this.customButtonTitle) return this - this.customButtonTitle = title; - this.customButtonDisabled = disabled; - this.customBoolean = callback; - this.customCallbackString = callbackClicked; - this.customBooleanClicked = false; + addCustomBoolean(customId: string, title: string, disabled: boolean, callback: () => Promise | null, callbackClicked: string | null, emoji?: string, initial?: boolean) { this.customButtons[customId] = { + title: title, + disabled: disabled, + value: callbackClicked, + emoji: emoji, + active: initial ?? false, + onClick: callback ?? (() => null), + response: null, + } return this; } addReasonButton(reason: string) { @@ -52,92 +50,80 @@ class confirmationMessage { } async send(editOnly?: boolean) { while (true) { + let fullComponents = [ + new Discord.MessageButton() + .setCustomId("yes") + .setLabel("Confirm") + .setStyle(this.inverted ? "SUCCESS" : "DANGER") + .setEmoji(getEmojiByName("CONTROL.TICK", "id")), + new Discord.MessageButton() + .setCustomId("no") + .setLabel("Cancel") + .setStyle("SECONDARY") + .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) + ] + Object.entries(this.customButtons).forEach(([k, v]) => { + fullComponents.push(new Discord.MessageButton() + .setCustomId(k) + .setLabel(v.title) + .setStyle(v.active ? "SUCCESS" : "PRIMARY") + .setEmoji(getEmojiByName(v.emoji, "id")) + .setDisabled(v.disabled)) + }) + if (this.reason !== null) fullComponents.push(new Discord.MessageButton() + .setCustomId("reason") + .setLabel(`Edit Reason`) + .setStyle("PRIMARY") + .setEmoji(getEmojiByName("ICONS.EDIT", "id")) + .setDisabled(false) + ) + let components = [] + for (let i = 0; i < fullComponents.length; i += 5) { + components.push(new MessageActionRow().addComponents(fullComponents.slice(i, i + 5))); + } let object = { embeds: [ new EmojiEmbed() .setEmoji(this.emoji) .setTitle(this.title) - .setDescription(this.description) + .setDescription(this.description + "\n\n" + Object.values(this.customButtons).map(v => { + if (v.value === null) return ""; + return v.active ? `*${v.value}*\n` : ""; + }).join("")) .setStatus(this.color) - .setFooter({text: (this.customBooleanClicked ?? this.customCallbackClicked) ? this.customCallbackString : ""}) - ], - components: [ - new MessageActionRow().addComponents([ - new Discord.MessageButton() - .setCustomId("yes") - .setLabel("Confirm") - .setStyle(this.inverted ? "SUCCESS" : "DANGER") - .setEmoji(getEmojiByName("CONTROL.TICK", "id")), - new Discord.MessageButton() - .setCustomId("no") - .setLabel("Cancel") - .setStyle("SECONDARY") - .setEmoji(getEmojiByName("CONTROL.CROSS", "id")) - ].concat(this.customButtonTitle ? [new Discord.MessageButton() - .setCustomId("custom") - .setLabel(this.customButtonTitle) - .setStyle(this.customBooleanClicked !== null ? - ( this.customBooleanClicked ? "SUCCESS" : "PRIMARY" ) : - "PRIMARY" - ) - .setDisabled(this.customButtonDisabled) - .setEmoji(getEmojiByName("CONTROL.TICKET", "id")) - ] : []) - .concat(this.reason !== null ? [new Discord.MessageButton() - .setCustomId("reason") - .setLabel(`Edit Reason`) - .setStyle("PRIMARY") - .setEmoji(getEmojiByName("ICONS.EDIT", "id")) - ] : [])) ], + components: components, ephemeral: true, fetchReply: true } let m; - if ( editOnly ) { - m = await this.interaction.editReply(object); - } else { - m = await this.interaction.reply(object) - } + try { + if ( editOnly ) { + m = await this.interaction.editReply(object); + } else { + m = await this.interaction.reply(object) + } + } catch { return { cancelled: true } } let component; try { component = await (m as Message).awaitMessageComponent({filter: (m) => m.user.id === this.interaction.user.id, time: 300000}); } catch (e) { - return { - success: false, - buttonClicked: this.customBooleanClicked ?? this.customCallbackClicked, - response: this.customCallbackResponse - }; + return { success: false, components: this.customButtons }; } if (component.customId === "yes") { component.deferUpdate(); - if (this.customBooleanClicked === true) this.customCallbackResponse = await this.customBoolean(); - return { - success: true, - buttonClicked: this.customBooleanClicked ?? this.customCallbackClicked, - response: this.customCallbackResponse + for (let [k, v] of Object.entries(this.customButtons)) { + if (!v.active) continue + try { v.response = await v.onClick(); } + catch (e) { console.log(e) } }; + return { success: true, components: this.customButtons }; } else if (component.customId === "no") { component.deferUpdate(); - return { - success: false, - buttonClicked: this.customBooleanClicked ?? this.customCallbackClicked, - response: this.customCallbackResponse - }; - } else if (component.customId === "custom") { - component.deferUpdate(); - if (this.customBooleanClicked !== null) { - this.customBooleanClicked = !this.customBooleanClicked; - } else { - this.customCallbackResponse = await this.customCallback(); - this.customCallbackClicked = true; - this.customButtonDisabled = true; - } - editOnly = true; + return { success: false, components: this.customButtons }; } else if (component.customId === "reason") { await component.showModal(new Discord.Modal().setCustomId("modal").setTitle(`Editing reason`).addComponents( - // @ts-ignore - new MessageActionRow().addComponents(new TextInputComponent() + new MessageActionRow().addComponents(new TextInputComponent() .setCustomId("reason") .setLabel("Reason") .setMaxLength(2000) @@ -163,10 +149,13 @@ class confirmationMessage { let out; try { out = await modalInteractionCollector(m, (m) => m.channel.id == this.interaction.channel.id, (m) => m.customId == "reason") - } catch (e) { continue } - if (out.fields) { - return {newReason: out.fields.getTextInputValue("reason") ?? ""}; - } else { return { newReason: this.reason } } + } catch (e) { return {} } + if (out.fields) { return { newReason: out.fields.getTextInputValue("reason") ?? "" }; } + else { return { newReason: this.reason } } + } else { + component.deferUpdate(); + this.customButtons[component.customId].active = !this.customButtons[component.customId].active; + return { components: this.customButtons }; } } } diff --git a/src/utils/convertCurlyBracketString.ts b/src/utils/convertCurlyBracketString.ts index 0277751..6ffdc8f 100644 --- a/src/utils/convertCurlyBracketString.ts +++ b/src/utils/convertCurlyBracketString.ts @@ -2,12 +2,12 @@ async function convertCurlyBracketString(str, memberID, memberName, serverName, let memberCount = (await members.fetch()).size let bots = (await members.fetch()).filter(m => m.user.bot).size str = str - .replace("{@}", `<@${memberID}>`) - .replace("{server}", `${serverName}`) - .replace("{name}", `${memberName}`) - .replace("{count}", `${memberCount}`) - .replace("{count:bots}", `${bots}`) - .replace("{count:humans}", `${memberCount - bots}`); + .replace("{member:mention}", memberID ? `<@${memberID}>` : "{member:mention}") + .replace("{member:name}", memberName ? `${memberName}` : "{member:name}") + .replace("{serverName}", serverName ? `${serverName}` : "{serverName}") + .replace("{memberCount}", memberCount ? `${memberCount}` : "{memberCount}") + .replace("{memberCount:bots}", bots ? `${bots}` : "{memberCount:bots}") + .replace("{memberCount:humans}", (memberCount && bots) ? `${memberCount - bots}` : "{memberCount:humans}"); return str } diff --git a/src/utils/database.ts b/src/utils/database.ts index c0ae9be..5b1d6d9 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -63,8 +63,13 @@ export class Guilds { } } - async remove(guild: string, key: string, value: any) { - if (Array.isArray(value)) { + async remove(guild: string, key: string, value: any, innerKey?: string) { + if (innerKey) { + await this.guilds.updateOne({ id: guild }, { + $pull: { [key]: { [innerKey]: { $eq: value } } } + }, { upsert: true }); + } + else if (Array.isArray(value)) { await this.guilds.updateOne({ id: guild }, { $pullAll: { [key]: value } }, { upsert: true }); @@ -200,11 +205,7 @@ export interface GuildConfig { channel: string | null, message: string | null, } - stats: { - enabled: boolean, - channel: string | null, - text: string | null, - }[] + stats: {} logging: { logs: { enabled: boolean,