diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban.ts index e32a720..7346bcc 100644 --- a/src/commands/mod/ban.ts +++ b/src/commands/mod/ban.ts @@ -53,6 +53,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, undefined, "The user will be sent a DM", + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/mod/info.ts b/src/commands/mod/info.ts index 12c414f..6e9abed 100644 --- a/src/commands/mod/info.ts +++ b/src/commands/mod/info.ts @@ -1,3 +1,4 @@ +import { LoadingEmbed } from './../../utils/defaultEmbeds.js'; import type { HistorySchema } from "../../utils/database.js"; import Discord, { CommandInteraction, @@ -8,11 +9,12 @@ import Discord, { ButtonBuilder, MessageComponentInteraction, ModalSubmitInteraction, - TextInputComponent, ButtonStyle, - StringSelectMenuInteraction + StringSelectMenuInteraction, + TextInputStyle, + APIMessageComponentEmoji } from "discord.js"; -import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; +import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; import getEmojiByName from "../../utils/getEmojiByName.js"; import client from "../../utils/client.js"; @@ -153,60 +155,59 @@ async function showHistory(member: Discord.GuildMember, interaction: CommandInte } } if (pageIndex === null) pageIndex = 0; - const components = ( - openFilterPane - ? [ - new ActionRowBuilder().addComponents( - new Discord.SelectMenuBuilder() - .setOptions( - Object.entries(types).map(([key, value]) => ({ - label: value.text, - value: key, - default: filteredTypes.includes(key), - emoji: client.emojis.resolve(getEmojiByName(value.emoji, "id")) - })) - ) - .setMinValues(1) - .setMaxValues(Object.keys(types).length) - .setCustomId("filter") - .setPlaceholder("Select at least one event") - ) - ] - : [] - ).concat([ - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setCustomId("prevYear") - .setLabel((currentYear - 1).toString()) - .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) - .setStyle(ButtonStyle.Secondary), - new ButtonBuilder().setCustomId("prevPage").setLabel("Previous page").setStyle(ButtonStyle.Primary), - new ButtonBuilder().setCustomId("today").setLabel("Today").setStyle(ButtonStyle.Primary), - new ButtonBuilder() - .setCustomId("nextPage") - .setLabel("Next page") - .setStyle(ButtonStyle.Primary) - .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()), - new ButtonBuilder() - .setCustomId("nextYear") - .setLabel((currentYear + 1).toString()) - .setEmoji(getEmojiByName("CONTROL.RIGHT", "id")) - .setStyle(ButtonStyle.Secondary) - .setDisabled(currentYear === new Date().getFullYear()) - ]), - new ActionRowBuilder().addComponents([ - new ButtonBuilder() - .setLabel("Mod notes") - .setCustomId("modNotes") - .setStyle(ButtonStyle.Primary) - .setEmoji(getEmojiByName("ICONS.EDIT", "id")), - new ButtonBuilder() - .setLabel("Filter") - .setCustomId("openFilter") - .setStyle(openFilterPane ? ButtonStyle.Success : ButtonStyle.Primary) - .setEmoji(getEmojiByName("ICONS.FILTER", "id")) - ]) - ]); + let components: (ActionRowBuilder | ActionRowBuilder)[] = [] + if (openFilterPane) components = components.concat([ + new ActionRowBuilder().addComponents( + new Discord.StringSelectMenuBuilder().setOptions( + ...Object.entries(types).map(([key, value]) => new StringSelectMenuOptionBuilder() + .setLabel(value.text) + .setValue(key) + .setDefault(filteredTypes.includes(key)) + .setEmoji(client.emojis.resolve(getEmojiByName(value.emoji, "id"))! as APIMessageComponentEmoji) + ) + ) + .setMinValues(1) + .setMaxValues(Object.keys(types).length) + .setCustomId("filter") + .setPlaceholder("Select events to show") + ) + ]) + components = components.concat([new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setCustomId("prevYear") + .setLabel((currentYear - 1).toString()) + .setEmoji(getEmojiByName("CONTROL.LEFT", "id")) + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId("prevPage") + .setLabel("Previous page") + .setStyle(ButtonStyle.Primary), + new ButtonBuilder().setCustomId("today").setLabel("Today").setStyle(ButtonStyle.Primary), + new ButtonBuilder() + .setCustomId("nextPage") + .setLabel("Next page") + .setStyle(ButtonStyle.Primary) + .setDisabled(pageIndex >= groups.length - 1 && currentYear === new Date().getFullYear()), + new ButtonBuilder() + .setCustomId("nextYear") + .setLabel((currentYear + 1).toString()) + .setEmoji(getEmojiByName("CONTROL.RIGHT", "id")) + .setStyle(ButtonStyle.Secondary) + .setDisabled(currentYear === new Date().getFullYear()) + ])]) + components = components.concat([new ActionRowBuilder().addComponents([ + new ButtonBuilder() + .setLabel("Mod notes") + .setCustomId("modNotes") + .setStyle(ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.EDIT", "id")), + new ButtonBuilder() + .setLabel("Filter") + .setCustomId("openFilter") + .setStyle(openFilterPane ? ButtonStyle.Success : ButtonStyle.Primary) + .setEmoji(getEmojiByName("ICONS.FILTER", "id")) + ])]) + const end = "\n\nJanuary " + currentYear.toString() + @@ -216,7 +217,7 @@ async function showHistory(member: Discord.GuildMember, interaction: CommandInte currentYear.toString(); if (groups.length > 0) { const toRender = groups[Math.min(pageIndex, groups.length - 1)]!; - m = (await interaction.editReply({ + m = await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("MEMBER.JOIN") @@ -226,25 +227,25 @@ async function showHistory(member: Discord.GuildMember, interaction: CommandInte ) .setStatus("Success") .setFooter({ - text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "" + text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "No filters selected" }) ], components: components - })) as Message; + }); } else { - m = (await interaction.editReply({ + m = await interaction.editReply({ embeds: [ new EmojiEmbed() .setEmoji("MEMBER.JOIN") .setTitle("Moderation history for " + member.user.username) - .setDescription(`**${currentYear}**\n\n*No events*` + "\n\n" + end) + .setDescription(`**${currentYear}**\n\n*No events*`) .setStatus("Success") .setFooter({ - text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "" + text: openFilterPane && filteredTypes.length ? "Filters are currently enabled" : "No filters selected" }) ], components: components - })) as Message; + }) } let i: MessageComponentInteraction; try { @@ -315,7 +316,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let m: Message; const member = interaction.options.getMember("user") as Discord.GuildMember; await interaction.reply({ - embeds: [new EmojiEmbed().setEmoji("NUCLEUS.LOADING").setTitle("Downloading Data").setStatus("Danger")], + embeds: LoadingEmbed, ephemeral: true, fetchReply: true }); @@ -324,9 +325,7 @@ const callback = async (interaction: CommandInteraction): Promise => { let timedOut = false; while (!timedOut) { note = await client.database.notes.read(member.guild.id, member.id); - if (firstLoad && !note) { - await showHistory(member, interaction); - } + if (firstLoad && !note) { await showHistory(member, interaction); } firstLoad = false; m = (await interaction.editReply({ embeds: [ @@ -344,7 +343,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setCustomId("modify") .setEmoji(getEmojiByName("ICONS.EDIT", "id")), new ButtonBuilder() - .setLabel("View moderation history") + .setLabel("Moderation history") .setStyle(ButtonStyle.Primary) .setCustomId("history") .setEmoji(getEmojiByName("ICONS.HISTORY", "id")) @@ -364,14 +363,14 @@ const callback = async (interaction: CommandInteraction): Promise => { .setCustomId("modal") .setTitle("Editing moderator note") .addComponents( - new ActionRowBuilder().addComponents( - new TextInputComponent() + new ActionRowBuilder().addComponents( + new Discord.TextInputBuilder() .setCustomId("note") .setLabel("Note") .setMaxLength(4000) .setRequired(false) - .setStyle("PARAGRAPH") - .setValue(note ? note : "") + .setStyle(TextInputStyle.Paragraph) + .setValue(note ? note : " ") ) ) ); @@ -408,7 +407,9 @@ const callback = async (interaction: CommandInteraction): Promise => { if (out === null) { continue; } else if (out instanceof ModalSubmitInteraction) { - const toAdd = out.fields.getTextInputValue("note") || null; + let toAdd = out.fields.getTextInputValue("note") || null; + if (toAdd === " ") toAdd = null; + if (toAdd) toAdd = toAdd.trim(); await client.database.notes.create(member.guild.id, member.id, toAdd); } else { continue; diff --git a/src/commands/mod/kick.ts b/src/commands/mod/kick.ts index 2feb5d7..dd71892 100644 --- a/src/commands/mod/kick.ts +++ b/src/commands/mod/kick.ts @@ -1,4 +1,4 @@ -import { LinkWarningFooter } from './../../utils/defaultEmbeds'; +import { LinkWarningFooter } from './../../utils/defaultEmbeds.js'; import { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; // @ts-expect-error import humanizeDuration from "humanize-duration"; @@ -42,6 +42,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, null, "The user will be sent a DM", + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/mod/mute.ts b/src/commands/mod/mute.ts index 0e76cba..95c8c5a 100644 --- a/src/commands/mod/mute.ts +++ b/src/commands/mod/mute.ts @@ -192,6 +192,7 @@ const callback = async (interaction: CommandInteraction): Promise => { async () => await create(interaction.guild, interaction.options.getUser("user")!, interaction.user, reason), "An appeal ticket will be created when Confirm is clicked", + null, "CONTROL.TICKET", createAppealTicket ) @@ -201,6 +202,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, undefined, null, + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts index b5e5554..0975375 100644 --- a/src/commands/mod/nick.ts +++ b/src/commands/mod/nick.ts @@ -45,6 +45,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, null, null, + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/mod/purge.ts b/src/commands/mod/purge.ts index e9aa41a..6087890 100644 --- a/src/commands/mod/purge.ts +++ b/src/commands/mod/purge.ts @@ -1,4 +1,4 @@ -import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle } from "discord.js"; +import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder } from "discord.js"; import type { SlashCommandSubcommandBuilder } from "@discordjs/builders"; import confirmationMessage from "../../utils/confirmationMessage.js"; import EmojiEmbed from "../../utils/generateEmojiEmbed.js"; @@ -26,7 +26,8 @@ const command = (builder: SlashCommandSubcommandBuilder) => ); const callback = async (interaction: CommandInteraction): Promise => { - const user = (interaction.options.getMember("user") as GuildMember); + if (!interaction.guild) return; + const user = (interaction.options.getMember("user") as GuildMember | null); const channel = interaction.channel as GuildChannel; if ( !["GUILD_TEXT", "GUILD_NEWS", "GUILD_NEWS_THREAD", "GUILD_PUBLIC_THREAD", "GUILD_PRIVATE_THREAD"].includes( @@ -46,7 +47,7 @@ const callback = async (interaction: CommandInteraction): Promise => { }); } // TODO:[Modals] Replace this with a modal - if (!interaction.options.getInteger("amount")) { + if (!interaction.options.get("amount")) { await interaction.reply({ embeds: [ new EmojiEmbed() @@ -74,17 +75,17 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStatus("Danger") ], components: [ - new Discord.ActionRowBuilder().addComponents([ + new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder().setCustomId("1").setLabel("1").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("3").setLabel("3").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("5").setLabel("5").setStyle(ButtonStyle.Secondary) ]), - new Discord.ActionRowBuilder().addComponents([ + new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder().setCustomId("10").setLabel("10").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("25").setLabel("25").setStyle(ButtonStyle.Secondary), new Discord.ButtonBuilder().setCustomId("50").setLabel("50").setStyle(ButtonStyle.Secondary) ]), - new Discord.ActionRowBuilder().addComponents([ + new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder() .setCustomId("done") .setLabel("Done") @@ -110,16 +111,14 @@ const callback = async (interaction: CommandInteraction): Promise => { } const amount = parseInt((await component).customId); - let messages; + let messages: Discord.Message[] = []; await (interaction.channel as TextChannel).messages.fetch({ limit: amount }).then(async (ms) => { if (user) { ms = ms.filter((m) => m.author.id === user.id); } - messages = await (channel as TextChannel).bulkDelete(ms, true); + messages = (await (channel as TextChannel).bulkDelete(ms, true)).map(m => m as Discord.Message); }); - if (messages) { - deleted = deleted.concat(messages.map((m) => m)); - } + deleted = deleted.concat(messages); } if (deleted.length === 0) return await interaction.editReply({ @@ -136,11 +135,12 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.history.create( "purge", interaction.guild.id, - user, - interaction.options.getString("reason"), + user.user, + interaction.user, + (interaction.options.get("reason")?.value as (string | null)) ?? "*No reason provided*", null, null, - deleted.length + deleted.length.toString() ); } const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; @@ -156,8 +156,8 @@ const callback = async (interaction: CommandInteraction): Promise => { list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), purgedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(interaction.channel.id, renderChannel(interaction.channel)), - messagesCleared: entry(deleted.length, deleted.length) + channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)), + messagesCleared: entry(deleted.length.toString(), deleted.length.toString()) }, hidden: { guild: interaction.guild.id @@ -189,7 +189,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStatus("Success") ], components: [ - new Discord.ActionRowBuilder().addComponents([ + new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder() .setCustomId("download") .setLabel("Download transcript") @@ -207,7 +207,7 @@ const callback = async (interaction: CommandInteraction): Promise => { } catch { return; } - if (component && component.customId === "download") { + if (component.customId === "download") { interaction.editReply({ embeds: [ new EmojiEmbed() @@ -239,12 +239,8 @@ const callback = async (interaction: CommandInteraction): Promise => { .setDescription( keyValueList({ channel: `<#${channel.id}>`, - amount: interaction.options.getInteger("amount").toString(), - reason: `\n> ${ - interaction.options.getString("reason") - ? interaction.options.getString("reason") - : "*No reason provided*" - }` + amount: (interaction.options.get("amount")?.value as number).toString(), + reason: `\n> ${interaction.options.get("reason")?.value ? interaction.options.get("reason")?.value : "*No reason provided*"}` }) ) .setColor("Danger") @@ -255,7 +251,7 @@ const callback = async (interaction: CommandInteraction): Promise => { try { if (!user) { const toDelete = await (interaction.channel as TextChannel).messages.fetch({ - limit: interaction.options.getInteger("amount") + limit: interaction.options.get("amount")?.value as number }); messages = await (channel as TextChannel).bulkDelete(toDelete, true); } else { @@ -265,7 +261,7 @@ const callback = async (interaction: CommandInteraction): Promise => { limit: 100 }) ).filter((m) => m.author.id === user.id) - ).first(interaction.options.getInteger("amount")); + ).first(interaction.options.get("amount")?.value as number); messages = await (channel as TextChannel).bulkDelete(toDelete, true); } } catch (e) { @@ -280,15 +276,29 @@ const callback = async (interaction: CommandInteraction): Promise => { components: [] }); } + if (!messages) { + await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setEmoji("CHANNEL.PURGE.RED") + .setTitle("Purge") + .setDescription("No messages could be deleted") + .setStatus("Danger") + ], + components: [] + }); + return; + } if (user) { await client.database.history.create( "purge", interaction.guild.id, - user, - interaction.options.getString("reason"), + user.user, + interaction.user, + (interaction.options.get("reason")?.value as (string | null)) ?? "*No reason provided*", null, null, - messages.size + messages.size.toString() ); } const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; @@ -304,8 +314,8 @@ const callback = async (interaction: CommandInteraction): Promise => { list: { memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), purgedBy: entry(interaction.user.id, renderUser(interaction.user)), - channel: entry(interaction.channel.id, renderChannel(interaction.channel)), - messagesCleared: entry(messages.size, messages.size) + channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as GuildChannel)), + messagesCleared: entry(messages.size.toString(), messages.size.toString()) }, hidden: { guild: interaction.guild.id @@ -314,14 +324,26 @@ const callback = async (interaction: CommandInteraction): Promise => { log(data); let out = ""; messages.reverse().forEach((message) => { - out += `${message.author.username}#${message.author.discriminator} (${message.author.id}) [${new Date( - message.createdTimestamp - ).toISOString()}]\n`; - const lines = message.content.split("\n"); - lines.forEach((line) => { - out += `> ${line}\n`; - }); - out += "\n\n"; + if (!message) { + out += "Unknown message\n\n" + } else { + const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" }; + out += `${author.username}#${author.discriminator} (${author.id}) [${new Date( + message.createdTimestamp + ).toISOString()}]\n`; + if (message.content) { + const lines = message.content.split("\n"); + lines.forEach((line) => { + out += `> ${line}\n`; + }); + } + if (message.attachments.size > 0) { + message.attachments.forEach((attachment) => { + out += `Attachment > ${attachment.url}\n`; + }); + } + out += "\n\n"; + } }); const attachmentObject = { attachment: Buffer.from(out), @@ -337,7 +359,7 @@ const callback = async (interaction: CommandInteraction): Promise => { .setStatus("Success") ], components: [ - new Discord.ActionRowBuilder().addComponents([ + new Discord.ActionRowBuilder().addComponents([ new Discord.ButtonBuilder() .setCustomId("download") .setLabel("Download transcript") @@ -355,7 +377,7 @@ const callback = async (interaction: CommandInteraction): Promise => { } catch { return; } - if (component && component.customId === "download") { + if (component.customId === "download") { interaction.editReply({ embeds: [ new EmojiEmbed() @@ -395,14 +417,15 @@ const callback = async (interaction: CommandInteraction): Promise => { }; const check = (interaction: CommandInteraction) => { + if (!interaction.guild) return false; const member = interaction.member as GuildMember; - const me = interaction.guild.me!; + const me = interaction.guild.members.me!; // Check if nucleus has the manage_messages permission - if (!me.permissions.has("MANAGE_MESSAGES")) throw new Error("I do not have the *Manage Messages* permission"); + if (!me.permissions.has("ManageMessages")) throw new Error("I do not have the *Manage Messages* permission"); // Allow the owner to purge if (member.id === interaction.guild.ownerId) return true; // Check if the user has manage_messages permission - if (!member.permissions.has("MANAGE_MESSAGES")) throw new Error("You do not have the *Manage Messages* permission"); + if (!member.permissions.has("ManageMessages")) throw new Error("You do not have the *Manage Messages* permission"); // Allow purge return true; }; diff --git a/src/commands/mod/softban.ts b/src/commands/mod/softban.ts index c9a71f6..12bfc3e 100644 --- a/src/commands/mod/softban.ts +++ b/src/commands/mod/softban.ts @@ -51,6 +51,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, null, null, + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/mod/unmute.ts b/src/commands/mod/unmute.ts index 40510a8..c93f8cc 100644 --- a/src/commands/mod/unmute.ts +++ b/src/commands/mod/unmute.ts @@ -38,6 +38,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, null, "The user will be sent a DM", + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts index 1c5223f..390baa5 100644 --- a/src/commands/mod/warn.ts +++ b/src/commands/mod/warn.ts @@ -41,6 +41,7 @@ const callback = async (interaction: CommandInteraction): Promise => { !(await areTicketsEnabled(interaction.guild.id)), async () => await create(interaction.guild!, interaction.options.getUser("user")!, interaction.user, reason), "An appeal ticket will be created", + null, "CONTROL.TICKET", createAppealTicket ) @@ -50,6 +51,7 @@ const callback = async (interaction: CommandInteraction): Promise => { false, null, "The user will be sent a DM", + null, "ICONS.NOTIFY." + (notify ? "ON" : "OFF"), notify ) diff --git a/src/commands/tag.ts b/src/commands/tag.ts index c54d7ab..859b7fc 100644 --- a/src/commands/tag.ts +++ b/src/commands/tag.ts @@ -2,6 +2,8 @@ import { AutocompleteInteraction, CommandInteraction, ActionRowBuilder, ButtonBu import { SlashCommandBuilder } from "@discordjs/builders"; import client from "../utils/client.js"; import EmojiEmbed from "../utils/generateEmojiEmbed.js"; +import { capitalize } from "../utils/generateKeyValueList.js"; +import { getResults } from "../utils/search.js"; const command = new SlashCommandBuilder() .setName("tag") @@ -9,41 +11,44 @@ const command = new SlashCommandBuilder() .addStringOption((o) => o.setName("tag").setDescription("The tag to get").setAutocomplete(true).setRequired(true)); const callback = async (interaction: CommandInteraction): Promise => { - const config = await client.database.guilds.read(interaction.guild.id); - const tags = config.getKey("tags"); - const tag = tags[interaction.options.getString("tag")]; + const config = await client.database.guilds.read(interaction.guild!.id); + const tags = config.tags; + const search = interaction.options.get("tag")?.value as string; + const tag = tags[search]; if (!tag) { - return await interaction.reply({ + await interaction.reply({ embeds: [ new EmojiEmbed() .setTitle("Tag") - .setDescription(`Tag \`${interaction.options.get("tag")}\` does not exist`) + .setDescription(`Tag \`${search}\` does not exist`) .setEmoji("PUNISH.NICKNAME.RED") .setStatus("Danger") ], ephemeral: true }); + return; } let url = ""; - let components: ActionRowBuilder[] = []; + let components: ActionRowBuilder[] = []; if (tag.match(/^(http|https):\/\/[^ "]+$/)) { url = tag; components = [ - new ActionRowBuilder().addComponents([new ButtonBuilder().setLabel("Open").setURL(url).setStyle(ButtonStyle.Link)]) + new ActionRowBuilder().addComponents([new ButtonBuilder().setLabel("Open").setURL(url).setStyle(ButtonStyle.Link)]) ]; } - return await interaction.reply({ - embeds: [ - new EmojiEmbed() - .setTitle(interaction.options.get("tag").value) - .setDescription(tag) - .setEmoji("PUNISH.NICKNAME.GREEN") - .setStatus("Success") - .setImage(url) - ], + const em = new EmojiEmbed() + .setTitle(capitalize(search)) + .setEmoji("PUNISH.NICKNAME.GREEN") + .setStatus("Success") + if (url) em.setImage(url) + else em.setDescription(tag); + + await interaction.reply({ + embeds: [em], components: components, ephemeral: true }); + return; }; const check = () => { @@ -52,9 +57,12 @@ const check = () => { 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; + const prompt = interaction.options.getString("tag"); + console.log(prompt) + const possible = Object.keys((await client.memory.readGuildInfo(interaction.guild.id)).tags); + const results = getResults(prompt ?? "", possible); + console.log(results) + return results; }; export { command }; diff --git a/src/commands/tags/create.ts b/src/commands/tags/create.ts index b922da3..5379bf8 100644 --- a/src/commands/tags/create.ts +++ b/src/commands/tags/create.ts @@ -16,8 +16,8 @@ const command = (builder: SlashCommandSubcommandBuilder) => ); const callback = async (interaction: CommandInteraction): Promise => { - const name = interaction.options.getString("name"); - const value = interaction.options.getString("value"); + const name = interaction.options.get("name")?.value as string; + const value = interaction.options.get("value")?.value as string; if (name!.length > 100) return await interaction.reply({ embeds: [ @@ -41,7 +41,7 @@ const callback = async (interaction: CommandInteraction): Promise => { ephemeral: true }); const data = await client.database.guilds.read(interaction.guild!.id); - if (data.tags.length >= 100) + if (Object.keys(data.tags).length >= 100) return await interaction.reply({ embeds: [ new EmojiEmbed() @@ -90,6 +90,7 @@ const callback = async (interaction: CommandInteraction): Promise => { await client.database.guilds.write(interaction.guild!.id, { [`tags.${name}`]: value }); + await client.memory.forceUpdate(interaction.guild!.id); } catch (e) { return await interaction.editReply({ embeds: [ @@ -116,7 +117,7 @@ const callback = async (interaction: CommandInteraction): Promise => { const check = (interaction: CommandInteraction) => { const member = interaction.member as Discord.GuildMember; - if (!member.permissions.has("MANAGE_MESSAGES")) + if (!member.permissions.has("ManageMessages")) throw new Error("You must have the *Manage Messages* permission to use this command"); return true; }; diff --git a/src/commands/tags/delete.ts b/src/commands/tags/delete.ts index 2fff637..6d76eb1 100644 --- a/src/commands/tags/delete.ts +++ b/src/commands/tags/delete.ts @@ -13,7 +13,7 @@ const command = (builder: SlashCommandSubcommandBuilder) => .addStringOption((o) => o.setName("name").setRequired(true).setDescription("The name of the tag")); const callback = async (interaction: CommandInteraction): Promise => { - const name = interaction.options.getString("name"); + const name = interaction.options.get("name")?.value as string; const data = await client.database.guilds.read(interaction.guild!.id); if (!data.tags[name]) return await interaction.reply({ @@ -51,6 +51,7 @@ const callback = async (interaction: CommandInteraction): Promise => { }); try { await client.database.guilds.write(interaction.guild!.id, null, ["tags." + name]); + await client.memory.forceUpdate(interaction.guild!.id); } catch (e) { console.log(e); return await interaction.editReply({ @@ -78,7 +79,7 @@ const callback = async (interaction: CommandInteraction): Promise => { const check = (interaction: CommandInteraction) => { const member = interaction.member as Discord.GuildMember; - if (!member.permissions.has("MANAGE_MESSAGES")) + if (!member.permissions.has("ManageMessages")) throw new Error("You must have the *Manage Messages* permission to use this command"); return true; }; diff --git a/src/commands/tags/edit.ts b/src/commands/tags/edit.ts index 376abbd..018a0bb 100644 --- a/src/commands/tags/edit.ts +++ b/src/commands/tags/edit.ts @@ -110,6 +110,7 @@ const callback = async (interaction: CommandInteraction): Promise => { toSet[`tags.${newname}`] = data.tags[name]; } await client.database.guilds.write(interaction.guild.id, toSet === {} ? null : toSet, toUnset); + await client.memory.forceUpdate(interaction.guild!.id); } catch (e) { return await interaction.editReply({ embeds: [ @@ -136,7 +137,7 @@ const callback = async (interaction: CommandInteraction): Promise => { const check = (interaction: CommandInteraction) => { const member = interaction.member as GuildMember; - if (!member.permissions.has("MANAGE_MESSAGES")) + if (!member.permissions.has("ManageMessages")) throw new Error("You must have the *Manage Messages* permission to use this command"); return true; }; diff --git a/src/commands/user/about.ts b/src/commands/user/about.ts index f94fb24..72ad1eb 100644 --- a/src/commands/user/about.ts +++ b/src/commands/user/about.ts @@ -1,4 +1,4 @@ -import { LoadingEmbed } from "./../../utils/defaultEmbeds.js"; +import { LoadingEmbed, Embed } from "./../../utils/defaultEmbeds.js"; import Discord, { CommandInteraction, GuildMember, @@ -25,30 +25,6 @@ const command = (builder: SlashCommandSubcommandBuilder) => option.setName("user").setDescription("The user to get info about | Default: Yourself") ); -class Embed { - embed: EmojiEmbed = new EmojiEmbed(); - title: string = ""; - description = ""; - pageId = 0; - - setEmbed(embed: EmojiEmbed) { - this.embed = embed; - return this; - } - setTitle(title: string) { - this.title = title; - return this; - } - setDescription(description: string) { - this.description = description; - return this; - } - setPageId(pageId: number) { - this.pageId = pageId; - return this; - } -} - const callback = async (interaction: CommandInteraction): Promise => { const guild = interaction.guild!; const member = (interaction.options.getMember("user") ?? interaction.member) as Discord.GuildMember; @@ -101,11 +77,11 @@ async function userAbout(guild: Discord.Guild, member: Discord.GuildMember, inte BugHunterLevel2: "Bug Hunter Level 2", Partner: "Partnered Server Owner", Staff: "Discord Staff", - VerifiedDeveloper: "Verified Bot Developer" - // ActiveDeveloper + VerifiedDeveloper: "Verified Bot Developer", + ActiveDeveloper: "Active Developer", + Quarantined: "Quarantined [What does this mean?](https://support.discord.com/hc/en-us/articles/6461420677527)", + Spammer: "Likely Spammer" // CertifiedModerator - // Quarantined https://discord-api-types.dev/api/discord-api-types-v10/enum/UserFlags#Quarantined - // Spammer https://discord-api-types.dev/api/discord-api-types-v10/enum/UserFlags#Spammer // VerifiedBot }; const members = await guild.members.fetch(); @@ -121,8 +97,8 @@ async function userAbout(guild: Discord.Guild, member: Discord.GuildMember, inte let s = ""; let count = 0; let ended = false; - for (const roleId in roles) { - const string = `<@&${roleId}>, `; + for (const roleId of roles) { + const string = `<@&${roleId[1].id}>, `; if (s.length + string.length > 1000) { ended = true; s += `and ${roles.size - count} more`; diff --git a/src/config/emojis.json b/src/config/emojis.json index f29f383..168d84b 100644 --- a/src/config/emojis.json +++ b/src/config/emojis.json @@ -190,7 +190,10 @@ "BugHunterLevel2": "775783766130950234", "Partner": "775783766178005033", "Staff": "775783766383788082", - "VerifiedDeveloper": "775783766425600060" + "VerifiedDeveloper": "775783766425600060", + "Quarantined": "1059794708638994474", + "Spammer": "1059794708638994474", + "ActiveDeveloper": "1059795592966053918" }, "VOICE": { "CONNECT": "784785219391193138", diff --git a/src/context/messages/purgeto.ts b/src/context/messages/purgeto.ts new file mode 100644 index 0000000..e2ec6e4 --- /dev/null +++ b/src/context/messages/purgeto.ts @@ -0,0 +1,279 @@ +import confirmationMessage from '../../utils/confirmationMessage.js'; +import EmojiEmbed from '../../utils/generateEmojiEmbed.js'; +import { LoadingEmbed } from './../../utils/defaultEmbeds.js'; +import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, MessageContextMenuCommandInteraction } from "discord.js"; +import client from "../../utils/client.js"; +import getEmojiByName from '../../utils/getEmojiByName.js'; + +const command = new ContextMenuCommandBuilder() + .setName("Purge up to here") + + +async function waitForButton(m: Discord.Message, member: Discord.GuildMember): Promise { + let component; + try { + component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id }); + } catch (e) { + return false; + } + (await component).deferUpdate(); + return true; +} + + +const callback = async (interaction: MessageContextMenuCommandInteraction) => { + await interaction.targetMessage.fetch(); + const targetMessage = interaction.targetMessage; + const targetMember: Discord.User = targetMessage.author; + let allowedMessage: Discord.Message | undefined = undefined; + const channel = interaction.channel; + if (!channel) return; + await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true }); + // Option for "include this message"? + // Option for "Only selected user"? + + const history: Discord.Collection = await channel.messages.fetch({ limit: 100 }); + if (Date.now() - targetMessage.createdTimestamp > 2 * 7 * 24 * 60 * 60 * 1000) { + const m = await interaction.editReply({ embeds: [new EmojiEmbed() + .setTitle("Purge") + .setDescription("The message you selected is older than 2 weeks. Discord only allows bots to delete messages that are 2 weeks old or younger.") + .setEmoji("CHANNEL.PURGE.RED") + .setStatus("Danger") + ], components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("oldest") + .setLabel("Select first allowed message") + .setStyle(ButtonStyle.Primary), + ) + ]}); + if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return; + } else if (!history.has(targetMessage.id)) { + const m = await interaction.editReply({ embeds: [new EmojiEmbed() + .setTitle("Purge") + .setDescription("The message you selected is not in the last 100 messages in this channel. Discord only allows bots to delete 100 messages at a time.") + .setEmoji("CHANNEL.PURGE.YELLOW") + .setStatus("Warning") + ], components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId("oldest") + .setLabel("Select first allowed message") + .setStyle(ButtonStyle.Primary), + ) + ]}); + if (!await waitForButton(m, interaction.member as Discord.GuildMember)) return; + } else { + allowedMessage = targetMessage; + } + + if (!allowedMessage) { + // Find the oldest message thats younger than 2 weeks + const messages = history.filter(m => Date.now() - m.createdTimestamp < 2 * 7 * 24 * 60 * 60 * 1000); + allowedMessage = messages.sort((a, b) => a.createdTimestamp - b.createdTimestamp).first(); + } + + if (!allowedMessage) { + await interaction.editReply({ embeds: [new EmojiEmbed() + .setTitle("Purge") + .setDescription("There are no valid messages in the last 100 messages. (No messages younger than 2 weeks)") + .setEmoji("CHANNEL.PURGE.RED") + .setStatus("Danger") + ], components: [] }); + return; + } + + let reason: string | null = null + let confirmation; + let chosen = false; + let timedOut = false; + let deleteSelected = true; + let deleteUser = false; + do { + confirmation = await new confirmationMessage(interaction) + .setEmoji("CHANNEL.PURGE.RED") + .setTitle("Purge") + .setDescription( + `[[Selected Message]](${allowedMessage.url})\n\n` + + (reason ? "\n> " + reason.replaceAll("\n", "\n> ") : "*No reason provided*") + "\n\n" + + `Are you sure you want to delete all messages from below the selected message?` + ) + .addCustomBoolean( + "includeSelected", + "Include selected message", + false, + undefined, + "The selected message will be deleted as well.", + "The selected message will not be deleted.", + "CONTROL." + (deleteSelected ? "TICK" : "CROSS"), + deleteSelected + ) + .addCustomBoolean( + "onlySelectedUser", + "Only selected user", + false, + undefined, + `Only messages from <@${targetMember.id}> will be deleted.`, + `All messages will be deleted.`, + "CONTROL." + (deleteUser ? "TICK" : "CROSS"), + deleteUser + ) + .setColor("Danger") + .addReasonButton(reason ?? "") + .send(true) + reason = reason ?? "" + if (confirmation.cancelled) timedOut = true; + else if (confirmation.success !== undefined) chosen = true; + else if (confirmation.newReason) reason = confirmation.newReason; + else if (confirmation.components) { + deleteSelected = confirmation.components["includeSelected"]!.active; + deleteUser = confirmation.components["onlySelectedUser"]!.active; + } + } while (!chosen && !timedOut); + if (timedOut) return; + if (!confirmation.success) { + await interaction.editReply({ embeds: [new EmojiEmbed() + .setTitle("Purge") + .setDescription("No changes were made") + .setEmoji("CHANNEL.PURGE.GREEN") + .setStatus("Success") + ], components: [] }); + return; + } + const filteredMessages = history + .filter(m => m.createdTimestamp >= allowedMessage!.createdTimestamp) // older than selected + .filter(m => deleteUser ? m.author.id === targetMember.id : true) // only selected user + .filter(m => deleteSelected ? true : m.id !== allowedMessage!.id) // include selected + + const deleted = await (channel as GuildTextBasedChannel).bulkDelete(filteredMessages, true); + if (deleted.size === 0) { + return await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setEmoji("CHANNEL.PURGE.RED") + .setTitle("Purge") + .setDescription("No messages were deleted") + .setStatus("Danger") + ], + components: [] + }); + } + if (deleteUser) { + await client.database.history.create( + "purge", + interaction.guild!.id, + targetMember, + interaction.user, + reason === "" ? "*No reason provided*" : reason, + null, + null, + deleted.size.toString() + ); + } + const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger; + const data = { + meta: { + type: "channelPurge", + displayName: "Channel Purged", + calculateType: "messageDelete", + color: NucleusColors.red, + emoji: "PUNISH.BAN.RED", + timestamp: new Date().getTime() + }, + list: { + memberId: entry(interaction.user.id, `\`${interaction.user.id}\``), + purgedBy: entry(interaction.user.id, renderUser(interaction.user)), + channel: entry(interaction.channel!.id, renderChannel(interaction.channel! as Discord.GuildChannel)), + messagesCleared: entry(deleted.size.toString(), deleted.size.toString()) + }, + hidden: { + guild: interaction.guild!.id + } + }; + log(data); + let out = ""; + deleted.reverse().forEach((message) => { + if (!message) { + out += "Unknown message\n\n" + } else { + const author = message.author ?? { username: "Unknown", discriminator: "0000", id: "Unknown" }; + out += `${author.username}#${author.discriminator} (${author.id}) [${new Date( + message.createdTimestamp + ).toISOString()}]\n`; + if (message.content) { + const lines = message.content.split("\n"); + lines.forEach((line) => { + out += `> ${line}\n`; + }); + } + if (message.attachments.size > 0) { + message.attachments.forEach((attachment) => { + out += `Attachment > ${attachment.url}\n`; + }); + } + out += "\n\n"; + } + }); + const attachmentObject = { + attachment: Buffer.from(out), + name: `purge-${channel.id}-${Date.now()}.txt`, + description: "Purge log" + }; + const m = (await interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setEmoji("CHANNEL.PURGE.GREEN") + .setTitle("Purge") + .setDescription("Messages cleared") + .setStatus("Success") + ], + components: [ + new Discord.ActionRowBuilder().addComponents([ + new Discord.ButtonBuilder() + .setCustomId("download") + .setLabel("Download transcript") + .setStyle(ButtonStyle.Success) + .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id")) + ]) + ] + })) as Discord.Message; + let component; + try { + component = await m.awaitMessageComponent({ + filter: (m) => m.user.id === interaction.user.id, + time: 300000 + }); + } catch { + return; + } + if (component.customId === "download") { + interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setEmoji("CHANNEL.PURGE.GREEN") + .setTitle("Purge") + .setDescription("Transcript uploaded above") + .setStatus("Success") + ], + components: [], + files: [attachmentObject] + }); + } else { + interaction.editReply({ + embeds: [ + new EmojiEmbed() + .setEmoji("CHANNEL.PURGE.GREEN") + .setTitle("Purge") + .setDescription("Messages cleared") + .setStatus("Success") + ], + components: [] + }); + } +} + +const check = async (_interaction: MessageContextMenuCommandInteraction) => { + return true; +} + +export { command, callback, check } diff --git a/src/context/users/userinfo.ts b/src/context/users/userinfo.ts new file mode 100644 index 0000000..3b1a6bd --- /dev/null +++ b/src/context/users/userinfo.ts @@ -0,0 +1,18 @@ +import { ContextMenuCommandBuilder, GuildMember, UserContextMenuCommandInteraction } from "discord.js"; +import { userAbout } from "../../commands/user/about.js"; + +const command = new ContextMenuCommandBuilder() + .setName("User info") + +const callback = async (interaction: UserContextMenuCommandInteraction) => { + const guild = interaction.guild! + let member = interaction.targetMember + if (!member) member = await guild.members.fetch(interaction.targetId) + await userAbout(guild, member as GuildMember, interaction) +} + +const check = async (_interaction: UserContextMenuCommandInteraction) => { + return true; +} + +export { command, callback, check } diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index ddc143b..1796146 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -3,71 +3,15 @@ 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"; -import type { AutocompleteInteraction, Interaction, MessageComponentInteraction } from "discord.js"; + +import type { Interaction, MessageComponentInteraction } from "discord.js"; import type { NucleusClient } from "../utils/client.js"; export const event = "interactionCreate"; -function getAutocomplete(typed: string, options: string[]): { name: string; value: string }[] { - 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 })); -} - -function generateStatsChannelAutocomplete(typed: string) { - const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"]; - const autocompletions = []; - const beforeLastOpenBracket = typed.match(/(.*){[^{}]{0,15}$/); - if (beforeLastOpenBracket !== null) { - for (const replacement of validReplacements) { - autocompletions.push(`${beforeLastOpenBracket[1]} {${replacement}}`); - } - } else { - for (const replacement of validReplacements) { - autocompletions.push(`${typed} {${replacement}}`); - } - } - return getAutocomplete(typed, autocompletions); -} -function generateWelcomeMessageAutocomplete(typed: string) { - const validReplacements = [ - "serverName", - "memberCount", - "memberCount:bots", - "memberCount:humans", - "member:mention", - "member:name" - ]; - const autocompletions = []; - const beforeLastOpenBracket = typed.match(/(.*){[^{}]{0,15}$/); - if (beforeLastOpenBracket !== null) { - for (const replacement of validReplacements) { - autocompletions.push(`${beforeLastOpenBracket[1]} {${replacement}}`); - } - } else { - for (const replacement of validReplacements) { - autocompletions.push(`${typed} {${replacement}}`); - } - } - return getAutocomplete(typed, autocompletions); -} async function interactionCreate(interaction: Interaction) { - if ( - interaction.type === "MESSAGE_COMPONENT" && - (interaction as MessageComponentInteraction).componentType === "BUTTON" - ) { + if (interaction.isButton()) { const int = interaction as MessageComponentInteraction; switch (int.customId) { case "rolemenu": { @@ -86,19 +30,19 @@ async function interactionCreate(interaction: Interaction) { return createTranscript(int); } } - } else if (interaction.type === "APPLICATION_COMMAND_AUTOCOMPLETE") { - const int = interaction as AutocompleteInteraction; - switch (`${int.commandName} ${int.options.getSubcommandGroup(false)} ${int.options.getSubcommand(false)}`) { - case "tag null null": { - return int.respond(getAutocomplete(int.options.getString("tag") ?? "", await tagAutocomplete(int))); - } - case "settings null stats": { - return int.respond(generateStatsChannelAutocomplete(int.options.getString("name") ?? "")); - } - case "settings null welcome": { - return int.respond(generateWelcomeMessageAutocomplete(int.options.getString("message") ?? "")); - } - } + // } else if (interaction.type === "APPLICATION_COMMAND_AUTOCOMPLETE") { + // const int = interaction as AutocompleteInteraction; + // switch (`${int.commandName} ${int.options.getSubcommandGroup(false)} ${int.options.getSubcommand(false)}`) { + // case "tag null null": { + // return int.respond(getAutocomplete(int.options.getString("tag") ?? "", await tagAutocomplete(int))); + // } + // case "settings null stats": { + // return int.respond(generateStatsChannelAutocomplete(int.options.getString("name") ?? "")); + // } + // case "settings null welcome": { + // return int.respond(generateWelcomeMessageAutocomplete(int.options.getString("message") ?? "")); + // } + // } } } diff --git a/src/reflex/statsChannelUpdate.ts b/src/reflex/statsChannelUpdate.ts index d807267..2e12429 100644 --- a/src/reflex/statsChannelUpdate.ts +++ b/src/reflex/statsChannelUpdate.ts @@ -12,10 +12,9 @@ interface PropSchema { 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); - if (!guild) return; user = user ?? member!.user; const config = await client.database.guilds.read(guild.id); - Object.entries(config.getKey("stats")).forEach(async ([channel, props]) => { + Object.entries(config.stats).forEach(async ([channel, props]) => { if ((props as PropSchema).enabled) { let string = (props as PropSchema).name; if (!string) return; @@ -27,13 +26,13 @@ export async function callback(client: NucleusClient, member?: GuildMember, guil fetchedChannel = null; } if (!fetchedChannel) { - const deleted = config.getKey("stats")[channel]; + const deleted = config.stats[channel]; await client.database.guilds.write(guild!.id, null, `stats.${channel}`); return singleNotify( "statsChannelDeleted", guild!.id, "One or more of your stats channels have been deleted. Please use `/settings stats` if you wish to add the channel again.\n" + - `The channels name was: ${deleted.name}`, + `The channels name was: ${deleted!.name}`, "Critical" ); } diff --git a/src/utils/client.ts b/src/utils/client.ts index 53267f2..a57c639 100644 --- a/src/utils/client.ts +++ b/src/utils/client.ts @@ -1,11 +1,10 @@ -import Discord, { Client, Interaction } from 'discord.js'; +import Discord, { Client, Interaction, AutocompleteInteraction } from 'discord.js'; import { Logger } from "../utils/log.js"; import Memory from "../utils/memory.js"; import type { VerifySchema } from "../reflex/verify.js"; import { Guilds, History, ModNotes, Premium } from "../utils/database.js"; import EventScheduler from "../utils/eventScheduler.js"; import type { RoleMenuSchema } from "../actions/roleMenu.js"; -// @ts-expect-error import config from "../config/main.json" assert { type: "json" }; @@ -28,9 +27,9 @@ class NucleusClient extends Client { ((builder: Discord.SlashCommandBuilder) => Discord.SlashCommandBuilder) | Discord.SlashCommandSubcommandBuilder | ((builder: Discord.SlashCommandSubcommandBuilder) => Discord.SlashCommandSubcommandBuilder) | Discord.SlashCommandSubcommandGroupBuilder | ((builder: Discord.SlashCommandSubcommandGroupBuilder) => Discord.SlashCommandSubcommandGroupBuilder), callback: (interaction: Interaction) => Promise, - check: (interaction: Interaction) => Promise | boolean + check: (interaction: Interaction) => Promise | boolean, + autocomplete: (interaction: AutocompleteInteraction) => Promise }> = {}; - // commands: Discord.Collection = new Discord.Collection(); constructor(database: typeof NucleusClient.prototype.database) { super({ intents: 32767 }); diff --git a/src/utils/commandRegistration/register.ts b/src/utils/commandRegistration/register.ts index 0193f6c..a4c57c4 100644 --- a/src/utils/commandRegistration/register.ts +++ b/src/utils/commandRegistration/register.ts @@ -1,5 +1,4 @@ import Discord, { Interaction, SlashCommandBuilder, ApplicationCommandType } from 'discord.js'; -// @ts-expect-error import config from "../../config/main.json" assert { type: "json" }; import client from "../client.js"; import fs from "fs"; @@ -140,19 +139,31 @@ async function registerCommandHandler() { const commandName = "contextCommands/message/" + interaction.commandName; execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction) return; + } else if (interaction.isAutocomplete()) { + const commandName = interaction.commandName; + const subcommandGroupName = interaction.options.getSubcommandGroup(false); + const subcommandName = interaction.options.getSubcommand(false); + + const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : ""); + + const choices = await client.commands[fullCommandName]?.autocomplete(interaction); + + const formatted = (choices ?? []).map(choice => { + return { name: choice, value: choice } + }) + interaction.respond(formatted) + } else if (interaction.isChatInputCommand()) { + const commandName = interaction.commandName; + const subcommandGroupName = interaction.options.getSubcommandGroup(false); + const subcommandName = interaction.options.getSubcommand(false); + + const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : ""); + + const command = client.commands[fullCommandName]; + const callback = command?.callback; + const check = command?.check; + execute(check, callback, interaction); } - if (!interaction.isChatInputCommand()) return; - - const commandName = interaction.commandName; - const subcommandGroupName = interaction.options.getSubcommandGroup(false); - const subcommandName = interaction.options.getSubcommand(false); - - const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : ""); - - const command = client.commands[fullCommandName]; - const callback = command?.callback; - const check = command?.check; - execute(check, callback, interaction); }); } diff --git a/src/utils/commandRegistration/slashCommandBuilder.ts b/src/utils/commandRegistration/slashCommandBuilder.ts index 45cb8f1..76ecabe 100644 --- a/src/utils/commandRegistration/slashCommandBuilder.ts +++ b/src/utils/commandRegistration/slashCommandBuilder.ts @@ -1,6 +1,5 @@ import type { SlashCommandSubcommandGroupBuilder } from "@discordjs/builders"; import type { SlashCommandBuilder } from "discord.js"; -// @ts-expect-error import config from "../../config/main.json" assert { type: "json" }; import getSubcommandsInFolder from "./getFilesInFolder.js"; import client from "../client.js"; diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts index 87724f3..6682be0 100644 --- a/src/utils/confirmationMessage.ts +++ b/src/utils/confirmationMessage.ts @@ -18,6 +18,7 @@ interface CustomBoolean { title: string; disabled: boolean; value: string | null; + notValue: string | null; emoji: string | undefined; active: boolean; onClick: () => Promise; @@ -68,6 +69,7 @@ class confirmationMessage { disabled: boolean, callback: (() => Promise) | null = async () => null, callbackClicked: string | null, + callbackNotClicked: string | null, emoji?: string, initial?: boolean ) { @@ -75,6 +77,7 @@ class confirmationMessage { title: title, disabled: disabled, value: callbackClicked, + notValue: callbackNotClicked, emoji: emoji, active: initial ?? false, onClick: callback ?? (async () => null), @@ -145,10 +148,12 @@ class confirmationMessage { "\n\n" + Object.values(this.customButtons) .map((v) => { - if (v.value === null) return ""; - return v.active ? `*${v.value}*\n` : ""; - }) - .join("") + if (v.active) { + return v.value ? `*${v.value}*\n` : ""; + } else { + return v.notValue ? `*${v.notValue}*\n` : ""; + } + }).join("") ) .setStatus(this.color) ], @@ -163,7 +168,8 @@ class confirmationMessage { } else { m = (await this.interaction.reply(object)) as unknown as Message; } - } catch { + } catch (e) { + console.log(e); cancelled = true; continue; } diff --git a/src/utils/database.ts b/src/utils/database.ts index 2624fc9..b14c5c4 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -1,6 +1,5 @@ import type Discord from "discord.js"; import { Collection, MongoClient } from "mongodb"; -// @ts-expect-error import config from "../config/main.json" assert { type: "json" }; const mongoClient = new MongoClient(config.mongoUrl); @@ -17,7 +16,6 @@ export class Guilds { } async setup(): Promise { - // @ts-expect-error this.defaultData = (await import("../config/default.json", { assert: { type: "json" } })) .default as unknown as GuildConfig; return this; diff --git a/src/utils/defaultEmbeds.ts b/src/utils/defaultEmbeds.ts index 39a6080..a027c76 100644 --- a/src/utils/defaultEmbeds.ts +++ b/src/utils/defaultEmbeds.ts @@ -8,4 +8,30 @@ export const LoadingEmbed = [ export const LinkWarningFooter = { text: "The button below will take you to a website set by the server moderators. Do not enter any passwords unless it is from a trusted website.", iconURL: "https://cdn.discordapp.com/emojis/952295894370369587.webp?size=128&quality=lossless" -} \ No newline at end of file +} + +class Embed { + embed: EmojiEmbed = new EmojiEmbed(); + title: string = ""; + description = ""; + pageId = 0; + + setEmbed(embed: EmojiEmbed) { + this.embed = embed; + return this; + } + setTitle(title: string) { + this.title = title; + return this; + } + setDescription(description: string) { + this.description = description; + return this; + } + setPageId(pageId: number) { + this.pageId = pageId; + return this; + } +} + +export { Embed }; diff --git a/src/utils/eventScheduler.ts b/src/utils/eventScheduler.ts index 0a32a1e..3c9d6ca 100644 --- a/src/utils/eventScheduler.ts +++ b/src/utils/eventScheduler.ts @@ -2,7 +2,6 @@ import { Agenda } from "@hokify/agenda"; import client from "./client.js"; import * as fs from "fs"; import * as path from "path"; -// @ts-expect-error import config from "../config/main.json" assert { type: "json" }; class EventScheduler { diff --git a/src/utils/getEmojiByName.ts b/src/utils/getEmojiByName.ts index 7824982..3fa2b53 100644 --- a/src/utils/getEmojiByName.ts +++ b/src/utils/getEmojiByName.ts @@ -1,4 +1,3 @@ -// @ts-expect-error import emojis from "../config/emojis.json" assert { type: "json" }; interface EmojisIndex { diff --git a/src/utils/memory.ts b/src/utils/memory.ts index a397d09..870ffaf 100644 --- a/src/utils/memory.ts +++ b/src/utils/memory.ts @@ -6,6 +6,7 @@ interface GuildData { filters: GuildConfig["filters"]; logging: GuildConfig["logging"]; tickets: GuildConfig["tickets"]; + tags: GuildConfig["tags"]; } class Memory { @@ -20,7 +21,7 @@ class Memory { } } }, 1000 * 60 * 30); - } + }; async readGuildInfo(guild: string): Promise { if (!this.memory.has(guild)) { @@ -29,10 +30,15 @@ class Memory { lastUpdated: Date.now(), filters: guildData.filters, logging: guildData.logging, - tickets: guildData.tickets + tickets: guildData.tickets, + tags: guildData.tags }); } return this.memory.get(guild)!; + }; + + async forceUpdate(guild: string) { + if (this.memory.has(guild)) this.memory.delete(guild); } } diff --git a/src/utils/search.ts b/src/utils/search.ts new file mode 100644 index 0000000..310dbf8 --- /dev/null +++ b/src/utils/search.ts @@ -0,0 +1,18 @@ +import Fuse from "fuse.js"; + +function getResults(typed: string, options: string[]): string[] { + 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() + // @ts-expect-error + const fuse = new Fuse(options, { + useExtendedSearch: true, + findAllMatches: true, + minMatchCharLength: typed.length > 3 ? 3 : typed.length, + }).search(typed); + return fuse.slice(0, 25).map((option: {item: string }) => option.item ); +} + +export { getResults } \ No newline at end of file