Development (#11)
We need this NOW.
---------
Co-authored-by: PineaFan <ash@pinea.dev>
Co-authored-by: pineafan <pineapplefanyt@gmail.com>
Co-authored-by: PineappleFan <PineaFan@users.noreply.github.com>
Co-authored-by: Skyler <skyler3665@gmail.com>
diff --git a/src/commands/help.ts b/src/commands/help.ts
index 767ca46..90ef133 100644
--- a/src/commands/help.ts
+++ b/src/commands/help.ts
@@ -1,23 +1,195 @@
-import { ActionRowBuilder, ButtonBuilder, ButtonStyle, CommandInteraction } from "discord.js";
-import { SlashCommandBuilder } from "@discordjs/builders";
+import {
+ ActionRowBuilder,
+ CommandInteraction,
+ StringSelectMenuBuilder,
+ ApplicationCommandOptionType,
+ ApplicationCommandType,
+ StringSelectMenuOptionBuilder,
+ SlashCommandBuilder,
+ StringSelectMenuInteraction,
+ ComponentType,
+ APIMessageComponentEmoji,
+ ApplicationCommandSubGroup,
+ PermissionsBitField,
+ Interaction,
+ ApplicationCommandOption,
+ ApplicationCommandSubCommand
+} from "discord.js";
+import client from "../utils/client.js";
+import EmojiEmbed from "../utils/generateEmojiEmbed.js";
+import { LoadingEmbed } from "../utils/defaults.js";
+import { capitalize } from "../utils/generateKeyValueList.js";
+import { getCommandByName, getCommandMentionByName } from "../utils/getCommandDataByName.js";
+import getEmojiByName from "../utils/getEmojiByName.js";
const command = new SlashCommandBuilder()
.setName("help")
.setDescription("Shows help for commands");
+const styles: Record<string, {emoji: string}> = {
+ "help": {emoji: "NUCLEUS.LOGO"},
+ "mod": {emoji: "PUNISH.BAN.RED"},
+ "nucleus": {emoji: "NUCLEUS.LOGO"},
+ "privacy": {emoji: "NUCLEUS.LOGO"},
+ "role": {emoji: "GUILD.ROLES.DELETE"},
+ "rolemenu": {emoji: "GUILD.ROLES.DELETE"},
+ "server": {emoji: "GUILD.RED"},
+ "settings": {emoji: "GUILD.SETTINGS.RED"},
+ "tag": {emoji: "PUNISH.NICKNAME.RED"},
+ "tags": {emoji: "PUNISH.NICKNAME.RED"},
+ "ticket": {emoji: "GUILD.TICKET.CLOSE"},
+ "user": {emoji: "MEMBER.LEAVE"},
+ "verify": {emoji: "CONTROL.REDTICK"}
+}
+
const callback = async (interaction: CommandInteraction): Promise<void> => {
- interaction.reply({components: [new ActionRowBuilder<ButtonBuilder>().addComponents(
- new ButtonBuilder()
- .setLabel("Create ticket")
- .setStyle(ButtonStyle.Primary)
- .setCustomId("createticket")
- )]}); // TODO: FINISH THIS FOR RELEASE
+ const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
+ const commands = client.fetchedCommands;
+
+ let closed = false;
+ let currentPath: [string, string, string] = ["", "", ""]
+ do {
+ const commandRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("commandRow")
+ .setPlaceholder("Select a command")
+ .addOptions(
+ ...commands.filter(command => command.type === ApplicationCommandType.ChatInput).map((command) => {
+ const builder = new StringSelectMenuOptionBuilder()
+ .setLabel(capitalize(command.name))
+ .setValue(command.name)
+ .setDescription(command.description)
+ .setDefault(currentPath[0] === command.name)
+ if (styles[command.name]) builder.setEmoji(getEmojiByName(styles[command.name]!.emoji, "id") as APIMessageComponentEmoji)
+ return builder
+ })
+ )
+ );
+ const subcommandGroupRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("subcommandGroupRow")
+ );
+ const subcommandRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("subcommandRow")
+ );
+ const embed = new EmojiEmbed()
+ .setTitle("Help")
+ .setStatus("Danger")
+ .setEmoji("NUCLEUS.LOGO")
+
+ if(currentPath[0] === "" || currentPath[0] === "help") {
+ embed.setDescription(
+ `Welcome to Nucleus\n\n` +
+ `Select a command to get started${
+ (interaction.member?.permissions as PermissionsBitField).has("ManageGuild") ?
+ `, or run ${getCommandMentionByName("nucleus/guide")} for commands to set up your server` : ``
+ }\n\n\n` +
+ `Nucleus is fully [open source](https://github.com/clicksminuteper/Nucleus), and all currently free features will remain free forever.\n\n` +
+ `You can invite Nucleus to your server using ${getCommandMentionByName("nucleus/invite")}`
+ )
+ } else {
+ const currentData = getCommandByName(currentPath.filter(value => value !== "" && value !== "none").join('/'));
+ const current = commands.find((command) => command.name === currentPath[0])!;
+
+ let optionString = ``
+ let options: (ApplicationCommandOption & {
+ nameLocalized?: string;
+ descriptionLocalized?: string;
+ })[] = [];
+ //options
+ if(currentPath[1] !== "" && currentPath[1] !== "none" && currentPath[2] !== "" && currentPath[2] !== "none") {
+ const Op = current.options.find(option => option.name === currentPath[1])! as ApplicationCommandSubGroup
+ const Op2 = Op.options!.find(option => option.name === currentPath[2])!
+ options = Op2.options ?? []
+ } else if(currentPath[1] !== "" && currentPath[1] !== "none") {
+ let Op = current.options.find(option => option.name === currentPath[1])!
+ if(Op.type === ApplicationCommandOptionType.SubcommandGroup) {
+ options = []
+ } else {
+ Op = Op as ApplicationCommandSubCommand
+ options = Op.options ?? []
+ }
+ } else {
+ options = current.options.filter(option => (option.type !== ApplicationCommandOptionType.SubcommandGroup) && (option.type !== ApplicationCommandOptionType.Subcommand));
+ }
+ for(const option of options) {
+ optionString += `> ${option.name} (${ApplicationCommandOptionType[option.type]})- ${option.description}\n`
+ }
+ const APICommand = client.commands["commands/" + currentPath.filter(value => value !== "" && value !== "none").join("/")]![0]
+ let allowedToRun = true;
+ if(APICommand?.check) {
+ allowedToRun = await APICommand.check(interaction as Interaction, true)
+ }
+ embed.setDescription(
+ `${getEmojiByName(styles[currentPath[0]]!.emoji)} **${capitalize(currentData.name)}**\n> ${currentData.mention}\n\n` +
+ `> ${currentData.description}\n\n` +
+ (APICommand ? (`${getEmojiByName(allowedToRun ? "CONTROL.TICK" : "CONTROL.CROSS")} You ${allowedToRun ? "" : "don't "}` +
+ `have permission to use this command\n\n`) : "") +
+ ((optionString.length > 0) ? "**Options:**\n" + optionString : "")
+ )
+ const subcommands = current.options.filter((option) => option.type === ApplicationCommandOptionType.Subcommand);
+ const subcommandGroups = current.options.filter((option) => option.type === ApplicationCommandOptionType.SubcommandGroup);
+
+ if(subcommandGroups.length > 0) {
+ subcommandGroupRow.components[0]!
+ .addOptions(
+ new StringSelectMenuOptionBuilder().setLabel("Select a subcommand").setValue("none").setDefault(currentPath[1] === "none"),
+ ...subcommandGroups.map((option) => new StringSelectMenuOptionBuilder().setLabel(capitalize(option.name)).setValue(option.name).setDefault(currentPath[1] === option.name))
+ )
+ if(subcommandGroupRow.components[0]!.options.find((option) => option.data.default && option.data.value !== "none")) {
+ const subsubcommands = (subcommandGroups.find((option) => option.name === currentPath[1])! as ApplicationCommandSubGroup).options ?? [];
+ subcommandRow.components[0]!
+ .addOptions(
+ new StringSelectMenuOptionBuilder().setLabel("Select a subcommand").setValue("none").setDefault(currentPath[2] === "none"),
+ ...subsubcommands.map((option) => new StringSelectMenuOptionBuilder().setLabel(capitalize(option.name)).setValue(option.name).setDefault(currentPath[2] === option.name))
+ )
+ }
+ }
+ if(subcommands.length > 0) {
+ subcommandGroupRow.components[0]!
+ .addOptions(
+ ...subcommands.map((option) => new StringSelectMenuOptionBuilder().setLabel(capitalize(option.name)).setValue(option.name).setDefault(currentPath[1] === option.name))
+ )
+ }
+ }
+
+ const cmps = [commandRow];
+ if(subcommandGroupRow.components[0]!.options.length > 0) cmps.push(subcommandGroupRow);
+ if(subcommandRow.components[0]!.options.length > 0) cmps.push(subcommandRow);
+
+ await interaction.editReply({ embeds: [embed], components: cmps });
+
+ let i: StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent<ComponentType.StringSelect>({filter: (newInteraction) => interaction.user.id === newInteraction.user.id,time: 300000})
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ const value = i.values[0]!;
+ switch(i.customId) {
+ case "commandRow": {
+ currentPath = [value, "", ""];
+ break;
+ }
+ case "subcommandGroupRow": {
+ currentPath = [currentPath[0], value , ""];
+ break;
+ }
+ case "subcommandRow": {
+ currentPath[2] = value;
+ break;
+ }
+ }
+
+ } while (!closed);
};
-const check = () => {
- return true;
-};
-export { command };
+export { command as command };
export { callback };
-export { check };
diff --git a/src/commands/mod/_meta.ts b/src/commands/mod/_meta.ts
index af8006c..c5fcca5 100644
--- a/src/commands/mod/_meta.ts
+++ b/src/commands/mod/_meta.ts
@@ -5,4 +5,4 @@
const subcommand = await command(name, description, `mod`);
-export { name, description, subcommand as command };
+export { name, description, subcommand as command };
\ No newline at end of file
diff --git a/src/commands/mod/about.ts b/src/commands/mod/about.ts
index 130cdbc..0a9d962 100644
--- a/src/commands/mod/about.ts
+++ b/src/commands/mod/about.ts
@@ -3,17 +3,16 @@
import Discord, {
CommandInteraction,
GuildMember,
- Interaction,
Message,
ActionRowBuilder,
ButtonBuilder,
MessageComponentInteraction,
ModalSubmitInteraction,
ButtonStyle,
- StringSelectMenuInteraction,
TextInputStyle,
+ APIMessageComponentEmoji,
+ SlashCommandSubcommandBuilder
} from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import client from "../../utils/client.js";
@@ -167,8 +166,7 @@
.setLabel(value.text)
.setValue(key)
.setDefault(filteredTypes.includes(key))
- // @ts-expect-error
- .setEmoji(getEmojiByName(value.emoji, "id")) // FIXME: This gives a type error but is valid
+ .setEmoji(getEmojiByName(value.emoji, "id") as APIMessageComponentEmoji)
)))
]);
components = components.concat([new ActionRowBuilder<Discord.ButtonBuilder>().addComponents([
@@ -253,7 +251,7 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
interaction.editReply({
@@ -269,9 +267,9 @@
timedOut = true;
continue;
}
- i.deferUpdate();
- if (i.customId === "filter") {
- filteredTypes = (i as StringSelectMenuInteraction).values;
+ await i.deferUpdate();
+ if (i.customId === "filter" && i.isStringSelectMenu()) {
+ filteredTypes = i.values;
pageIndex = null;
refresh = true;
}
@@ -359,7 +357,7 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
@@ -402,17 +400,12 @@
});
let out;
try {
- out = await modalInteractionCollector(
- m,
- (m: Interaction) =>
- (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === interaction.channelId,
- (m) => m.customId === "modify"
- );
+ out = await modalInteractionCollector(m, interaction.user);
} catch (e) {
timedOut = true;
continue;
}
- if (out === null) {
+ if (out === null || out.isButton()) {
continue;
} else if (out instanceof ModalSubmitInteraction) {
let toAdd = out.fields.getTextInputValue("note") || null;
@@ -423,7 +416,7 @@
continue;
}
} else if (i.customId === "history") {
- i.deferUpdate();
+ await i.deferUpdate();
if (!(await showHistory(member, interaction))) return;
}
}
@@ -436,6 +429,8 @@
return true;
};
-export { command };
-export { callback };
-export { check };
+export { command, callback, check };
+export const metadata = {
+ longDescription: "Shows the moderation history (all previous bans, kicks, warns etc.), and moderator notes for a user.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/ban.ts b/src/commands/mod/ban.ts
index 70e904c..e8309fb 100644
--- a/src/commands/mod/ban.ts
+++ b/src/commands/mod/ban.ts
@@ -1,11 +1,11 @@
-import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, User, ButtonStyle } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, User, ButtonStyle, SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import addPlurals from "../../utils/plurals.js";
import client from "../../utils/client.js";
import { LinkWarningFooter } from "../../utils/defaults.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
@@ -26,7 +26,7 @@
const callback = async (interaction: CommandInteraction): Promise<void> => {
if (!interaction.guild) return;
const { renderUser } = client.logger;
- // TODO:[Modals] Replace this with a modal
+ // TODO:[Modals] Replace the command arguments with a modal
let reason = null;
let notify = true;
let confirmation;
@@ -123,17 +123,20 @@
calculateType: "guildMemberPunish",
color: NucleusColors.red,
emoji: "PUNISH.BAN.RED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.user.id, `\`${member.user.id}\``),
name: entry(member.user.id, renderUser(member.user)),
- banned: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())),
+ banned: entry(Date.now().toString(), renderDelta(Date.now())),
bannedBy: entry(interaction.user.id, renderUser(interaction.user)),
reason: entry(reason, reason ? `\n> ${reason}` : "*No reason provided.*"),
accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)),
serverMemberCount: interaction.guild.memberCount
},
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
+ },
hidden: {
guild: interaction.guild.id
}
@@ -166,9 +169,12 @@
});
};
-const check = async (interaction: CommandInteraction) => {
+const check = async (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
const member = interaction.member as GuildMember;
+ // Check if the user has ban_members permission
+ if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission";
+ if(partial) return true;
const me = interaction.guild.members.me!;
let apply = interaction.options.getUser("user") as User | GuildMember;
const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
@@ -181,21 +187,23 @@
apply = apply as User
}
// Do not allow banning the owner
- if (member.id === interaction.guild.ownerId) throw new Error("You cannot ban the owner of the server");
+ if (member.id === interaction.guild.ownerId) return "You cannot ban the owner of the server";
// Check if Nucleus can ban the member
- if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
+ if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`;
// Check if Nucleus has permission to ban
- if (!me.permissions.has("BanMembers")) throw new Error("I do not have the *Ban Members* permission");
+ if (!me.permissions.has("BanMembers")) return "I do not have the *Ban Members* permission";
// Do not allow banning Nucleus
- if (member.id === me.id) throw new Error("I cannot ban myself");
+ if (member.id === me.id) return "I cannot ban myself";
// Allow the owner to ban anyone
if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has ban_members permission
- if (!member.permissions.has("BanMembers")) throw new Error("You do not have the *Ban Members* permission");
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow ban
return true;
};
export { command, callback, check };
+export const metadata = {
+ longDescription: "Removes a member from the server - this will prevent them from rejoining until they are unbanned, and will delete a specified number of days of messages from them.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/kick.ts b/src/commands/mod/kick.ts
index 380bcc9..059bdb2 100644
--- a/src/commands/mod/kick.ts
+++ b/src/commands/mod/kick.ts
@@ -1,13 +1,13 @@
import { LinkWarningFooter } from '../../utils/defaults.js';
-import { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
+import { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandSubcommandBuilder } from "discord.js";
// @ts-expect-error
import humanizeDuration from "humanize-duration";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
import type Discord from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import client from "../../utils/client.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
@@ -102,8 +102,8 @@
await client.database.history.create("kick", interaction.guild.id, member.user, interaction.user, reason);
const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
const timeInServer = member.joinedTimestamp ? entry(
- (new Date().getTime() - member.joinedTimestamp).toString(),
- humanizeDuration(new Date().getTime() - member.joinedTimestamp, {
+ (Date.now() - member.joinedTimestamp).toString(),
+ humanizeDuration(Date.now() - member.joinedTimestamp, {
round: true
})
) : entry(null, "*Unknown*")
@@ -114,18 +114,21 @@
calculateType: "guildMemberPunish",
color: NucleusColors.red,
emoji: "PUNISH.KICK.RED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.id, `\`${member.id}\``),
name: entry(member.id, renderUser(member.user)),
joined: undefined as (unknown | typeof entry),
- kicked: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())),
+ kicked: entry(Date.now().toString(), renderDelta(Date.now())),
kickedBy: entry(interaction.user.id, renderUser(interaction.user)),
reason: entry(reason, reason ? `\n> ${reason}` : "*No reason provided.*"),
timeInServer: timeInServer,
serverMemberCount: member.guild.memberCount
},
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
+ },
hidden: {
guild: member.guild.id
}
@@ -168,30 +171,37 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
+
const member = interaction.member as GuildMember;
+ // Check if the user has kick_members permission
+ if (!member.permissions.has("KickMembers")) return "You do not have the *Kick Members* permission";
+ if (partial) return true;
+
const me = interaction.guild.members.me!;
const apply = interaction.options.getMember("user") as GuildMember;
const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
const mePos = me.roles.cache.size > 1 ? me.roles.highest.position : 0;
const applyPos = apply.roles.cache.size > 1 ? apply.roles.highest.position : 0;
+ // Check if Nucleus has permission to kick
+ if (!me.permissions.has("KickMembers")) return "I do not have the *Kick Members* permission";
+ // Allow the owner to kick anyone
+ if (member.id === interaction.guild.ownerId) return true;
// Do not allow kicking the owner
if (member.id === interaction.guild.ownerId) return "You cannot kick the owner of the server";
// Check if Nucleus can kick the member
- if (!(mePos > applyPos)) return "I do not have a role higher than that member";
- // Check if Nucleus has permission to kick
- if (!me.permissions.has("KickMembers")) return "I do not have the *Kick Members* permission";
+ if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`;
// Do not allow kicking Nucleus
if (member.id === interaction.guild.members.me!.id) return "I cannot kick myself";
- // Allow the owner to kick anyone
- if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has kick_members permission
- if (!member.permissions.has("KickMembers")) return "You do not have the *Kick Members* permission";
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) return "You do not have a role higher than that member";
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow kick
return true;
};
export { command, callback, check };
+export const metadata = {
+ longDescription: "Removes a member from the server. They will be able to rejoin if they have an invite link.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/mute.ts b/src/commands/mod/mute.ts
index 86291e5..c795456 100644
--- a/src/commands/mod/mute.ts
+++ b/src/commands/mod/mute.ts
@@ -1,6 +1,6 @@
import { LinkWarningFooter, LoadingEmbed } from "../../utils/defaults.js";
import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
@@ -103,7 +103,7 @@
let component;
try {
component = await m.awaitMessageComponent({
- filter: (m) => m.user.id === interaction.user.id,
+ filter: (i) => {return i.user.id === interaction.user.id && i.channelId === interaction.channelId},
time: 300000
});
} catch {
@@ -235,8 +235,8 @@
.setDescription(
`You have been muted in ${interaction.guild.name}` +
(reason ? ` for:\n${reason}` : ".\n*No reason was provided*") + "\n\n" +
- `You will be unmuted at: <t:${Math.round(new Date().getTime() / 1000) + muteTime}:D> at ` +
- `<t:${Math.round(new Date().getTime() / 1000) + muteTime}:T> (<t:${Math.round(new Date().getTime() / 1000) + muteTime
+ `You will be unmuted at: <t:${Math.round(Date.now() / 1000) + muteTime}:D> at ` +
+ `<t:${Math.round(Date.now() / 1000) + muteTime}:T> (<t:${Math.round(Date.now() / 1000) + muteTime
}:R>)` + "\n\n" +
(createAppealTicket
? `You can appeal this in the ticket created in <#${confirmation.components!["appeal"]!.response}>`
@@ -267,10 +267,10 @@
await member.timeout(muteTime * 1000, reason || "*No reason provided*");
if (config.moderation.mute.role !== null) {
await member.roles.add(config.moderation.mute.role);
- await client.database.eventScheduler.schedule("naturalUnmute", (new Date().getTime() + muteTime * 1000).toString(), {
+ await client.database.eventScheduler.schedule("naturalUnmute", (Date.now() + muteTime * 1000).toString(), {
guild: interaction.guild.id,
user: member.id,
- expires: new Date().getTime() + muteTime * 1000
+ expires: Date.now() + muteTime * 1000
});
}
} else {
@@ -282,7 +282,7 @@
try {
if (config.moderation.mute.role !== null) {
await member.roles.add(config.moderation.mute.role);
- await client.database.eventScheduler.schedule("unmuteRole", (new Date().getTime() + muteTime * 1000).toString(), {
+ await client.database.eventScheduler.schedule("unmuteRole", (Date.now() + muteTime * 1000).toString(), {
guild: interaction.guild.id,
user: member.id,
role: config.moderation.mute.role
@@ -325,19 +325,22 @@
calculateType: "guildMemberPunish",
color: NucleusColors.yellow,
emoji: "PUNISH.WARN.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.user.id, `\`${member.user.id}\``),
name: entry(member.user.id, renderUser(member.user)),
mutedUntil: entry(
- (new Date().getTime() + muteTime * 1000).toString(),
- renderDelta(new Date().getTime() + muteTime * 1000)
+ (Date.now() + muteTime * 1000).toString(),
+ renderDelta(Date.now() + muteTime * 1000)
),
- muted: entry(new Date().getTime.toString(), renderDelta(new Date().getTime() - 1000)),
+ muted: entry(new Date().getTime.toString(), renderDelta(Date.now() - 1000)),
mutedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)),
reason: entry(reason, reason ? reason : "*No reason provided*")
},
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
+ },
hidden: {
guild: interaction.guild.id
}
@@ -361,9 +364,12 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = async (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
const member = interaction.member as GuildMember;
+ // Check if the user has moderate_members permission
+ if (!member.permissions.has("ModerateMembers")) return "You do not have the *Moderate Members* permission";
+ if (partial) return true;
const me = interaction.guild.members.me!;
const apply = interaction.options.getMember("user") as GuildMember;
const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
@@ -372,20 +378,21 @@
// Do not allow muting the owner
if (member.id === interaction.guild.ownerId) return "You cannot mute the owner of the server";
// Check if Nucleus can mute the member
- if (!(mePos > applyPos)) return "I do not have a role higher than that member";
+ if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`;
// Check if Nucleus has permission to mute
if (!me.permissions.has("ModerateMembers")) return "I do not have the *Moderate Members* permission";
// Do not allow muting Nucleus
if (member.id === me.id) return "I cannot mute myself";
// Allow the owner to mute anyone
if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has moderate_members permission
- if (!member.permissions.has("ModerateMembers"))
- return "You do not have the *Moderate Members* permission";
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) return "You do not have a role higher than that member";
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow mute
return true;
};
export { command, callback, check };
+export const metadata = {
+ longDescription: "Stops a member from being able to send messages or join voice channels for a specified amount of time.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/nick.ts b/src/commands/mod/nick.ts
index 9dd9336..5511d19 100644
--- a/src/commands/mod/nick.ts
+++ b/src/commands/mod/nick.ts
@@ -1,16 +1,16 @@
import { LinkWarningFooter } from './../../utils/defaults.js';
import { ActionRowBuilder, ButtonBuilder, CommandInteraction, GuildMember, ButtonStyle, Message } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import client from "../../utils/client.js";
import { areTicketsEnabled, create } from "../../actions/createModActionTicket.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
const command = (builder: SlashCommandSubcommandBuilder) => builder
.setName("nick")
- // .setNameLocalizations({"ru": "name", "zh-CN": "nickname"})
.setDescription("Changes a users nickname")
.addUserOption((option) => option.setName("user").setDescription("The user to change").setRequired(true))
.addStringOption((option) =>
@@ -156,15 +156,18 @@
calculateType: "guildMemberUpdate",
color: NucleusColors.yellow,
emoji: "PUNISH.NICKNAME.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.id, `\`${member.id}\``),
before: entry(before, before ?? "*No nickname set*"),
after: entry(nickname ?? null, nickname ?? "*No nickname set*"),
- updated: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ updated: entry(Date.now(), renderDelta(Date.now())),
updatedBy: entry(interaction.user.id, renderUser(interaction.user))
},
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
+ },
hidden: {
guild: interaction.guild!.id
}
@@ -189,8 +192,11 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = async (interaction: CommandInteraction, partial: boolean = false) => {
const member = interaction.member as GuildMember;
+ // Check if the user has manage_nicknames permission
+ if (!member.permissions.has("ManageNicknames")) return "You do not have the *Manage Nicknames* permission";
+ if (partial) return true;
const me = interaction.guild!.members.me!;
const apply = interaction.options.getMember("user") as GuildMember;
const memberPos = member.roles.cache.size ? member.roles.highest.position : 0;
@@ -200,20 +206,21 @@
// Do not allow any changing of the owner
if (member.id === interaction.guild.ownerId) return "You cannot change the owner's nickname";
// Check if Nucleus can change the nickname
- if (!(mePos > applyPos)) return "I do not have a role higher than that member";
+ if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`;
// Check if Nucleus has permission to change the nickname
if (!me.permissions.has("ManageNicknames")) return "I do not have the *Manage Nicknames* permission";
// Allow the owner to change anyone's nickname
if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has manage_nicknames permission
- if (!member.permissions.has("ManageNicknames"))
- return "You do not have the *Manage Nicknames* permission";
// Allow changing your own nickname
if (member === apply) return true;
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) return "You do not have a role higher than that member";
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow change
return true;
};
export { command, callback, check };
+export const metadata = {
+ longDescription: "Changes the nickname of a member. This is the name that shows in the member list and on messages.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/purge.ts b/src/commands/mod/purge.ts
index e6b4670..8644e26 100644
--- a/src/commands/mod/purge.ts
+++ b/src/commands/mod/purge.ts
@@ -1,6 +1,5 @@
-import { JSONTranscriptFromMessageArray, JSONTranscriptToHumanReadable } from '../../utils/logTranscripts.js';
-import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import Discord, { CommandInteraction, GuildChannel, GuildMember, TextChannel, ButtonStyle, ButtonBuilder, Message } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
@@ -30,7 +29,7 @@
if (!interaction.guild) return;
const user = (interaction.options.getMember("user") as GuildMember | null);
const channel = interaction.channel as GuildChannel;
- if (channel.isTextBased()) {
+ if (!channel.isTextBased()) {
return await interaction.reply({
embeds: [
new EmojiEmbed()
@@ -94,7 +93,7 @@
let component;
try {
component = m.awaitMessageComponent({
- filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id,
+ filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
time: 300000
});
} catch (e) {
@@ -148,7 +147,7 @@
calculateType: "messageDelete",
color: NucleusColors.red,
emoji: "CHANNEL.PURGE.RED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
@@ -161,7 +160,8 @@
}
};
log(data);
- const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(deleted)!);
+ const newOut = await client.database.transcripts.createTranscript(deleted, interaction, interaction.member as GuildMember);
+ const transcript = client.database.transcripts.toHumanReadable(newOut);
const attachmentObject = {
attachment: Buffer.from(transcript),
name: `purge-${channel.id}-${Date.now()}.txt`,
@@ -188,7 +188,7 @@
let component;
try {
component = await m.awaitMessageComponent({
- filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id,
+ filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
time: 300000
});
} catch {
@@ -296,7 +296,7 @@
calculateType: "messageDelete",
color: NucleusColors.red,
emoji: "CHANNEL.PURGE.RED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
@@ -309,35 +309,17 @@
}
};
log(data);
- let out = "";
- messages.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({
+ const messageArray: Message[] = messages.filter(message => !(
+ message!.components.some(
+ component => component.components.some(
+ child => child.customId?.includes("transcript") ?? false
+ )
+ )
+ )).map(message => message as Message);
+ const newOut = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember);
+
+ const code = await client.database.transcripts.create(newOut);
+ await interaction.editReply({
embeds: [
new EmojiEmbed()
.setEmoji("CHANNEL.PURGE.GREEN")
@@ -347,62 +329,30 @@
],
components: [
new Discord.ActionRowBuilder<ButtonBuilder>().addComponents([
- new Discord.ButtonBuilder()
- .setCustomId("download")
- .setLabel("Download transcript")
- .setStyle(ButtonStyle.Success)
- .setEmoji(getEmojiByName("CONTROL.DOWNLOAD", "id"))
+ new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript?code=${code}`),
])
]
- })) as Discord.Message;
- let component;
- try {
- component = await m.awaitMessageComponent({
- filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.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 = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return false;
const member = interaction.member as GuildMember;
+ // Check if the user has manage_messages permission
+ if (!member.permissions.has("ManageMessages")) return "You do not have the *Manage Messages* permission";
+ if (partial) return true;
const me = interaction.guild.members.me!;
// Check if nucleus has the manage_messages permission
if (!me.permissions.has("ManageMessages")) return "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("ManageMessages")) return "You do not have the *Manage Messages* permission";
// Allow purge
return true;
};
export { command, callback, check };
+export const metadata = {
+ longDescription: "Deletes a specified amount of messages from a channel, optionally from a specific user. Without an amount, you can repeatedly choose a number of messages to delete.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/slowmode.ts b/src/commands/mod/slowmode.ts
index 9792827..f282e82 100644
--- a/src/commands/mod/slowmode.ts
+++ b/src/commands/mod/slowmode.ts
@@ -1,7 +1,7 @@
// @ts-expect-error
import humanizeDuration from "humanize-duration";
import type { CommandInteraction, GuildMember, TextChannel } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
@@ -47,7 +47,7 @@
}) + "Are you sure you want to set the slowmode in this channel?"
)
.setColor("Danger")
- .setFailedMessage("No changes were made", "Danger", "CHANNEL.SLOWMODE.ON")
+ .setFailedMessage("No changes were made", "Success", "CHANNEL.SLOWMODE.ON")
.send();
if (confirmation.cancelled || !confirmation.success) return;
try {
@@ -76,14 +76,19 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
const member = interaction.member as GuildMember;
- // Check if Nucleus can set the slowmode
- if (!interaction.guild!.members.me!.permissions.has("ManageChannels")) return "I do not have the *Manage Channels* permission";
// Check if the user has manage_channel permission
if (!member.permissions.has("ManageChannels")) return "You do not have the *Manage Channels* permission";
+ if (partial) return true;
+ // Check if Nucleus can set the slowmode
+ if (!interaction.guild!.members.me!.permissions.has("ManageChannels")) return "I do not have the *Manage Channels* permission";
// Allow slowmode
return true;
};
export { command, callback, check };
+export const metadata = {
+ longDescription: "Stops members from being able to send messages without waiting a certain amount of time between messages.",
+ premiumOnly: true,
+}
diff --git a/src/commands/mod/softban.ts b/src/commands/mod/softban.ts
index 2787e91..1b404c9 100644
--- a/src/commands/mod/softban.ts
+++ b/src/commands/mod/softban.ts
@@ -1,11 +1,12 @@
import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, User, ButtonStyle } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import addPlurals from "../../utils/plurals.js";
import client from "../../utils/client.js";
import { LinkWarningFooter } from "../../utils/defaults.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
@@ -124,17 +125,20 @@
calculateType: "guildMemberPunish",
color: NucleusColors.yellow,
emoji: "PUNISH.BAN.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.user.id, `\`${member.user.id}\``),
name: entry(member.user.id, renderUser(member.user)),
- softbanned: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())),
+ softbanned: entry(Date.now().toString(), renderDelta(Date.now())),
softbannedBy: entry(interaction.user.id, renderUser(interaction.user)),
reason: entry(reason, reason ? `\n> ${reason}` : "*No reason provided.*"),
accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)),
serverMemberCount: interaction.guild.memberCount
},
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
+ },
hidden: {
guild: interaction.guild.id
}
@@ -167,9 +171,12 @@
});
};
-const check = async (interaction: CommandInteraction) => {
+const check = async (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
const member = interaction.member as GuildMember;
+ // Check if the user has ban_members permission
+ if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission";
+ if (partial) return true;
const me = interaction.guild.members.me!;
let apply = interaction.options.getUser("user") as User | GuildMember;
const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
@@ -182,19 +189,17 @@
apply = apply as User
}
// Do not allow banning the owner
- if (member.id === interaction.guild.ownerId) throw new Error("You cannot softban the owner of the server");
+ if (member.id === interaction.guild.ownerId) return "You cannot softban the owner of the server";
// Check if Nucleus can ban the member
- if (!(mePos > applyPos)) throw new Error("I do not have a role higher than that member");
+ if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`;
// Check if Nucleus has permission to ban
- if (!me.permissions.has("BanMembers")) throw new Error("I do not have the *Ban Members* permission");
+ if (!me.permissions.has("BanMembers")) return "I do not have the *Ban Members* permission";
// Do not allow banning Nucleus
- if (member.id === me.id) throw new Error("I cannot softban myself");
+ if (member.id === me.id) return "I cannot softban myself";
// Allow the owner to ban anyone
if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has ban_members permission
- if (!member.permissions.has("BanMembers")) throw new Error("You do not have the *Ban Members* permission");
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) throw new Error("You do not have a role higher than that member");
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow ban
return true;
};
diff --git a/src/commands/mod/unban.ts b/src/commands/mod/unban.ts
index 37fee99..40f4504 100644
--- a/src/commands/mod/unban.ts
+++ b/src/commands/mod/unban.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction, GuildMember, User } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
@@ -57,12 +57,12 @@
calculateType: "guildMemberPunish",
color: NucleusColors.green,
emoji: "PUNISH.BAN.GREEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.id, `\`${member.id}\``),
name: entry(member.id, renderUser(member)),
- unbanned: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ unbanned: entry(Date.now(), renderDelta(Date.now())),
unbannedBy: entry(interaction.user.id, renderUser(interaction.user)),
accountCreated: entry(member.createdTimestamp, renderDelta(member.createdTimestamp))
},
@@ -107,16 +107,17 @@
}
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
const member = interaction.member as GuildMember;
+ // Check if the user has ban_members permission
+ if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission";
+ if (partial) return true;
const me = interaction.guild.members.me!;
// Check if Nucleus can unban members
if (!me.permissions.has("BanMembers")) return "I do not have the *Ban Members* permission";
// Allow the owner to unban anyone
if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has ban_members permission
- if (!member.permissions.has("BanMembers")) return "You do not have the *Ban Members* permission";
// Allow unban
return true;
};
diff --git a/src/commands/mod/unmute.ts b/src/commands/mod/unmute.ts
index e2585e1..8562c4c 100644
--- a/src/commands/mod/unmute.ts
+++ b/src/commands/mod/unmute.ts
@@ -1,9 +1,10 @@
import type { CommandInteraction, GuildMember } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import client from "../../utils/client.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
@@ -105,14 +106,17 @@
calculateType: "guildMemberPunish",
color: NucleusColors.green,
emoji: "PUNISH.MUTE.GREEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.user.id, `\`${member.user.id}\``),
name: entry(member.user.id, renderUser(member.user)),
- unmuted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())),
+ unmuted: entry(Date.now().toString(), renderDelta(Date.now())),
unmutedBy: entry(interaction.user.id, renderUser(interaction.user))
},
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
+ },
hidden: {
guild: interaction.guild.id
}
@@ -131,9 +135,13 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
const member = interaction.member as GuildMember;
+ // Check if the user has moderate_members permission
+ if (!member.permissions.has("ModerateMembers"))
+ return "You do not have the *Moderate Members* permission";
+ if (partial) return true;
const me = interaction.guild.members.me!;
const apply = interaction.options.getMember("user") as GuildMember;
const memberPos = member.roles.cache.size > 1 ? member.roles.highest.position : 0;
@@ -142,16 +150,13 @@
// Do not allow unmuting the owner
if (member.id === interaction.guild.ownerId) return "You cannot unmute the owner of the server";
// Check if Nucleus can unmute the member
- if (!(mePos > applyPos)) return "I do not have a role higher than that member";
+ if (!(mePos > applyPos)) return `I do not have a role higher than <@${apply.id}>`;
// Check if Nucleus has permission to unmute
if (!me.permissions.has("ModerateMembers")) return "I do not have the *Moderate Members* permission";
// Allow the owner to unmute anyone
if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has moderate_members permission
- if (!member.permissions.has("ModerateMembers"))
- return "You do not have the *Moderate Members* permission";
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) return "You do not have a role higher than that member";
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow unmute
return true;
};
diff --git a/src/commands/mod/viewas.ts b/src/commands/mod/viewas.ts
index b176dd4..ef62816 100644
--- a/src/commands/mod/viewas.ts
+++ b/src/commands/mod/viewas.ts
@@ -7,9 +7,10 @@
ButtonStyle,
NonThreadGuildBasedChannel,
StringSelectMenuOptionBuilder,
- StringSelectMenuBuilder
+ StringSelectMenuBuilder,
+ APIMessageComponentEmoji
} from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import type { GuildBasedChannel } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
@@ -126,8 +127,7 @@
return new StringSelectMenuOptionBuilder()
.setLabel(c)
.setValue((set * 25 + i).toString())
- // @ts-expect-error
- .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id")) // Again, this is valid but TS doesn't think so
+ .setEmoji(getEmojiByName("ICONS.CHANNEL.CATEGORY", "id") as APIMessageComponentEmoji) // Again, this is valid but TS doesn't think so
.setDefault((set * 25 + i) === page)
}))
)}
@@ -157,19 +157,19 @@
});
let i;
try {
- i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id, time: 30000});
+ i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 30000});
} catch (e) {
closed = true;
continue;
}
- i.deferUpdate();
+ await i.deferUpdate();
if (i.customId === "back") page--;
else if (i.customId === "right") page++;
else if (i.customId === "category" && i.isStringSelectMenu()) page = parseInt(i.values[0]!);
}
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as GuildMember;
if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
return true;
diff --git a/src/commands/mod/warn.ts b/src/commands/mod/warn.ts
index 38aa4ad..ea4f084 100644
--- a/src/commands/mod/warn.ts
+++ b/src/commands/mod/warn.ts
@@ -1,10 +1,11 @@
import Discord, { CommandInteraction, GuildMember, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import keyValueList from "../../utils/generateKeyValueList.js";
import { create, areTicketsEnabled } from "../../actions/createModActionTicket.js";
import client from "../../utils/client.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
import { LinkWarningFooter } from "../../utils/defaults.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
@@ -116,7 +117,7 @@
calculateType: "guildMemberPunish",
color: NucleusColors.yellow,
emoji: "PUNISH.WARN.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
user: entry(
@@ -124,7 +125,10 @@
renderUser((interaction.options.getMember("user") as GuildMember).user)
),
warnedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)),
- reason: reason ? `\n> ${reason}` : "*No reason provided*"
+ reason: reason ? reason : "*No reason provided*"
+ },
+ separate: {
+ end: getEmojiByName("ICONS.NOTIFY." + (notify ? "ON" : "OFF")) + ` The user was ${notify ? "" : "not "}notified`
},
hidden: {
guild: interaction.guild.id
@@ -186,7 +190,7 @@
let component;
try {
component = await m.awaitMessageComponent({
- filter: (m) => m.user.id === interaction.user.id && m.channel!.id === interaction.channel!.id,
+ filter: (i) => i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.id === m.id,
time: 300000
});
} catch (e) {
@@ -275,9 +279,12 @@
}
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
if (!interaction.guild) return;
const member = interaction.member as GuildMember;
+ if (!member.permissions.has("ModerateMembers"))
+ return "You do not have the *Moderate Members* permission";
+ if(partial) return true;
const apply = interaction.options.getMember("user") as GuildMember | null;
if (apply === null) return "That member is not in the server";
const memberPos = member.roles.cache.size ? member.roles.highest.position : 0;
@@ -287,10 +294,8 @@
// Allow the owner to warn anyone
if (member.id === interaction.guild.ownerId) return true;
// Check if the user has moderate_members permission
- if (!member.permissions.has("ModerateMembers"))
- return "You do not have the *Moderate Members* permission";
// Check if the user is below on the role list
- if (!(memberPos > applyPos)) return "You do not have a role higher than that member";
+ if (!(memberPos > applyPos)) return `You do not have a role higher than <@${apply.id}>`;
// Allow warn
return true;
};
diff --git a/src/commands/nucleus/_meta.ts b/src/commands/nucleus/_meta.ts
index 521b338..bd7fd14 100644
--- a/src/commands/nucleus/_meta.ts
+++ b/src/commands/nucleus/_meta.ts
@@ -3,8 +3,6 @@
const name = "nucleus";
const description = "Commands relating to Nucleus itself";
-const subcommand = await command(name, description, `nucleus`)
+const subcommand = await command(name, description, `nucleus`, undefined, undefined, undefined, undefined, true);
-const allowedInDMs = true;
-
-export { name, description, subcommand as command, allowedInDMs };
+export { name, description, subcommand as command };
diff --git a/src/commands/nucleus/guide.ts b/src/commands/nucleus/guide.ts
index d3370ba..270ee62 100644
--- a/src/commands/nucleus/guide.ts
+++ b/src/commands/nucleus/guide.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction } from 'discord.js';
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import guide from "../../reflex/guide.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
@@ -9,10 +9,5 @@
guide(interaction.guild!, interaction);
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/nucleus/invite.ts b/src/commands/nucleus/invite.ts
index fd65e51..b89425a 100644
--- a/src/commands/nucleus/invite.ts
+++ b/src/commands/nucleus/invite.ts
@@ -1,5 +1,5 @@
import { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
@@ -29,10 +29,5 @@
});
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/nucleus/ping.ts b/src/commands/nucleus/ping.ts
index 12f1c6b..3e02a8f 100644
--- a/src/commands/nucleus/ping.ts
+++ b/src/commands/nucleus/ping.ts
@@ -1,6 +1,6 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
@@ -10,9 +10,9 @@
const callback = async (interaction: CommandInteraction): Promise<void> => {
// WEBSOCKET | Nucleus -> Discord
// EDITING | Nucleus -> discord -> nucleus | edit time / 2
- const initial = new Date().getTime();
+ const initial = Date.now();
await interaction.reply({ embeds: LoadingEmbed, ephemeral: true });
- const ping = new Date().getTime() - initial;
+ const ping = Date.now() - initial;
interaction.editReply({
embeds: [
new EmojiEmbed()
@@ -28,10 +28,5 @@
});
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/nucleus/premium.ts b/src/commands/nucleus/premium.ts
index 745f167..c431c8e 100644
--- a/src/commands/nucleus/premium.ts
+++ b/src/commands/nucleus/premium.ts
@@ -1,32 +1,211 @@
-import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import { ActionRowBuilder, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, ComponentType, Message, StringSelectMenuBuilder, StringSelectMenuInteraction } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import client from "../../utils/client.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder.setName("premium").setDescription("Information about Nucleus Premium");
+//TODO: Allow User to remove Premium
+
+const dmcallback = async (interaction: CommandInteraction, firstDescription: string, msg: Message): Promise<void> => {
+ let closed = false;
+ do {
+ const dbUser = await client.database.premium.fetchUser(interaction.user.id);
+ if(!dbUser) {
+ await interaction.editReply({embeds: [
+ new EmojiEmbed()
+ .setTitle("Premium")
+ .setDescription(`*You do not have premium! You can't activate premium on any servers.*` + firstDescription)
+ .setEmoji("NUCLEUS.LOGO")
+ .setStatus("Danger")
+ ]});
+ return;
+ }
+ const premiumGuilds = dbUser.appliesTo.map((guildID) => {
+ const guild = client.guilds.cache.get(guildID);
+ if(!guild) return undefined;
+ return guild.name;
+ });
+
+ const options = premiumGuilds.filter((guild) => guild !== undefined) as string[];
+
+ const removeRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("currentPremium")
+ .setPlaceholder("Select a server to remove premium from")
+ .setDisabled(premiumGuilds.length === 0)
+ .addOptions(options.slice(0, Math.min(options.length, 24)).map((guild) => {
+ return {label: guild, value: guild}
+ }))
+ );
+ const cancel = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("cancel")
+ .setLabel("Close")
+ .setStyle(ButtonStyle.Danger)
+ );
+
+ const components: ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>[] = [cancel];
+ if(options.length > 0) components.unshift(removeRow);
+ await interaction.editReply(
+ {
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Premium")
+ .setDescription(
+ `*You have premium on the following servers:*\n\n` +
+ (options.length > 0 ? options.join(', ') : `You have not activated premium in any guilds`) +
+ firstDescription)
+ .setEmoji("NUCLEUS.LOGO")
+ .setStatus("Success")
+ ],
+ components: components
+ });
+
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ const filter = (i: StringSelectMenuInteraction | ButtonInteraction) => i.user.id === interaction.user.id;
+ i = await msg.awaitMessageComponent<ComponentType.StringSelect | ComponentType.Button>({time: 300000, filter})
+ } catch (e) {
+ await interaction.deleteReply();
+ closed = true;
+ break;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ closed = true;
+ } else {
+ const response = client.database.premium.removePremium(interaction.user.id, i.values[0]!);
+ console.log(response)
+ }
+ } while (!closed);
+ await interaction.deleteReply();
+}
const callback = async (interaction: CommandInteraction): Promise<void> => {
- interaction.reply({
+ if (interaction.guild) client.database.premium.hasPremium(interaction.guild.id).finally(() => {});
+ const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true})
+ const member = await (await interaction.client.guilds.fetch("684492926528651336")).members.fetch(interaction.user.id).catch(() => {
+ interaction.editReply({ embeds: [
+ new EmojiEmbed()
+ .setTitle("Premium")
+ .setDescription(`*You are not currently in the Clicks Server. To gain access to premium please join.*` + firstDescription)
+ .setEmoji("NUCLEUS.LOGO")
+ .setStatus("Danger")
+ ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder().setStyle(ButtonStyle.Link).setLabel("Join").setURL("https://discord.gg/bPaNnxe"))] });
+ })
+ if (!member) return;
+ const firstDescription = "\n\nPremium allows servers of your choice to get access to extra features for a fixed price per month.\nThis includes:\n" +
+ `${getEmojiByName("MOD.IMAGES.TOOSMALL")} Attachment logs - Stores attachments so they can be viewed after a message is deleted.\n` +
+ `${getEmojiByName("GUILD.TICKET.ARCHIVED")} Ticket Transcripts - Gives a link to view the history of a ticket after it has been closed.\n`
+ const dbMember = await client.database.premium.fetchUser(interaction.user.id)
+ let premium = `You do not have premium! You can't activate premium on any servers.`;
+ let count = 0;
+ const {level, appliesTo} = dbMember ?? {level: 0, appliesTo: []}
+ if (level === 99) {
+ premium = `You have Infinite Premium! You have been gifted this by the developers as a thank you. You can give premium to any and all servers you are in.`;
+ count = 200;
+ } else if (level === 1) {
+ premium = `You have Premium tier 1! You can give premium to ${1 - appliesTo.length} more server(s).`;
+ count = 1;
+ } else if (level === 2) {
+ premium = `You have Premium tier 2! You can give premium to ${3 - appliesTo.length} more server(s).`;
+ count = 3;
+ } else if (level === 3) {
+ premium = `You have Premium Mod! You can give premium to ${3 - appliesTo.length} more server(s), as well as automatically giving premium to all servers you have a "manage" permission in.`
+ count = 3;
+ }
+ if (dbMember?.expiresAt) {
+ premium = `**You can't give servers premium anymore because your subscription ended or was cancelled.** To get premium again please subscribe in the Clicks server`
+ count = 0;
+ }
+ if(!interaction.guild) return await dmcallback(interaction, firstDescription, m);
+ const hasPremium = await client.database.premium.hasPremium(interaction.guild!.id);
+ let premiumGuild = ""
+ if (hasPremium) {
+ premiumGuild = `**This server has premium! It was ${hasPremium[2] === 3 && hasPremium[3] ? `automatically applied by <@${hasPremium[1]}>` : `given by <@${hasPremium[1]}>`}**\n\n`
+ }
+
+ const components: ActionRowBuilder<ButtonBuilder>[] = []
+ if (level === 0 || dbMember?.expiresAt) {
+ components.push(
+ new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setStyle(ButtonStyle.Link)
+ .setLabel("Join Clicks")
+ .setURL("https://discord.gg/bPaNnxe")
+ )
+ )
+ } else {
+ components.push(
+ new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setStyle(premiumGuild.length > 0 ? ButtonStyle.Secondary : ButtonStyle.Success)
+ .setLabel(premiumGuild.length > 0 ? "This server has premium" : "Activate premium here")
+ .setCustomId("premiumActivate")
+ .setDisabled(count <= 0 || (hasPremium ? hasPremium[0] : false))
+ )
+ )
+ }
+
+ interaction.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Premium")
.setDescription(
- "*Nucleus Premium is currently not available.*\n\n" +
- "Premium allows your server to get access to extra features, for a fixed price per month.\nThis includes:\n" +
- "- Attachment logs - Stores attachments so they can be viewed after a message is deleted.\n" +
- "- Ticket Transcripts - Gives a link to view the history of a ticket after it has been closed.\n"
+ premiumGuild + premium + firstDescription
)
.setEmoji("NUCLEUS.LOGO")
.setStatus("Danger")
+ .setImage("https://assets.clicks.codes/ads/ads/nucleus-premium.png")
],
- ephemeral: true
+ components: components
});
-};
-const check = () => {
- return true;
+ const filter = (i: ButtonInteraction) => i.customId === "premiumActivate" && i.user.id === interaction.user.id;
+ let i;
+ try {
+ i = await interaction.channel!.awaitMessageComponent<2>({ filter, time: 60000 });
+ } catch (e) {
+ return;
+ }
+ i.deferUpdate();
+ const guild = i.guild!;
+ if (count - appliesTo.length <= 0) {
+ interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Premium")
+ .setDescription(
+ `You have already activated premium on the maximum amount of servers!` + firstDescription
+ )
+ .setEmoji("NUCLEUS.PREMIUMACTIVATE")
+ .setStatus("Danger")
+ ],
+ components: []
+ });
+ } else {
+ await client.database.premium.addPremium(interaction.user.id, guild.id);
+ interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Premium")
+ .setDescription(
+ `You have activated premium on this server!` + firstDescription
+ )
+ .setEmoji("NUCLEUS.LOGO")
+ .setStatus("Danger")
+ ],
+ components: []
+ });
+ }
};
export { command };
export { callback };
-export { check };
diff --git a/src/commands/nucleus/stats.ts b/src/commands/nucleus/stats.ts
index d8b2807..19c0949 100644
--- a/src/commands/nucleus/stats.ts
+++ b/src/commands/nucleus/stats.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
@@ -13,16 +13,11 @@
.setTitle("Stats")
.setDescription(`**Servers:** ${client.guilds.cache.size}\n` + `**Ping:** \`${client.ws.ping * 2}ms\``)
.setStatus("Success")
- .setEmoji("GUILD.GRAPHS")
+ .setEmoji("SETTINGS.STATS.GREEN")
],
ephemeral: true
});
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/nucleus/suggest.ts b/src/commands/nucleus/suggest.ts
index de0e69b..6ba3445 100644
--- a/src/commands/nucleus/suggest.ts
+++ b/src/commands/nucleus/suggest.ts
@@ -1,7 +1,7 @@
import { LoadingEmbed } from '../../utils/defaults.js';
import { ButtonStyle, CommandInteraction } from "discord.js";
import Discord from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
@@ -66,10 +66,5 @@
});
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/privacy.ts b/src/commands/privacy.ts
index 9100302..46784f5 100644
--- a/src/commands/privacy.ts
+++ b/src/commands/privacy.ts
@@ -1,6 +1,5 @@
import { LoadingEmbed } from "../utils/defaults.js";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, SelectMenuOptionBuilder, StringSelectMenuBuilder } from "discord.js";
-import { SlashCommandBuilder } from "@discordjs/builders";
+import Discord, { SlashCommandBuilder, CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, SelectMenuOptionBuilder, StringSelectMenuBuilder } from "discord.js";
import EmojiEmbed from "../utils/generateEmojiEmbed.js";
import getEmojiByName from "../utils/getEmojiByName.js";
import createPageIndicator from "../utils/createPageIndicator.js";
@@ -22,8 +21,7 @@
.setDescription(
"Nucleus is a bot that naturally needs to store data about servers.\n" +
"We are entirely [open source](https://github.com/ClicksMinutePer/Nucleus), so you can check exactly what we store, and how it works.\n\n" +
- "If you are a server administrator, you can view the options page in the dropdown under this message.\n\n" +
- "Any questions about Nucleus, how it works and data stored can be asked in [our server](https://discord.gg/bPaNnxe)."
+ "Any questions about Nucleus, how it works, and what data is stored can be asked in [our server](https://discord.gg/bPaNnxe)."
)
.setEmoji("NUCLEUS.LOGO")
.setStatus("Danger")
@@ -50,39 +48,37 @@
new EmojiEmbed()
.setTitle("Link scanning and Transcripts")
.setDescription(
- "**Facebook** - Facebook trackers include data such as your date of birth, and guess your age if not entered, your preferences, who you interact with and more.\n" +
- "**AMP** - AMP is a technology that allows websites to be served by Google. This means Google can store and track data, and are pushing this to as many pages as possible.\n\n" +
- "Transcripts allow you to store all messages sent in a channel. This could be an issue in some cases, as they are hosted on [Pastebin](https://pastebin.com), so a leaked link could show all messages sent in the channel.\n" // TODO: Not on pastebin
+ "Transcripts allow you to store all messages sent in a channel. Transcripts are stored in our database along with the rest of the server's settings but is accessible by anyone with the link, so a leaked link could show all messages sent in the channel.\n"
)
.setEmoji("NUCLEUS.LOGO")
.setStatus("Danger")
)
.setTitle("Link scanning and Transcripts")
- .setDescription("Regarding Facebook and AMP filter types, and ticket transcripts")
+ .setDescription("Information about how links and images are scanned, and transcripts are stored")
.setPageId(2)
].concat(
(interaction.member as Discord.GuildMember).permissions.has("Administrator")
? [
- new Embed()
- .setEmbed(
- new EmojiEmbed()
- .setTitle("Options")
- .setDescription("Below are buttons for controlling this servers privacy settings")
- .setEmoji("NUCLEUS.LOGO")
- .setStatus("Danger")
- )
- .setTitle("Options")
- .setDescription("Options")
- .setPageId(3)
- .setComponents([
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Clear all data")
- .setCustomId("clear-all-data")
- .setStyle(ButtonStyle.Danger)
- ])
- ])
- ]
+ new Embed()
+ .setEmbed(
+ new EmojiEmbed()
+ .setTitle("Options")
+ .setDescription("Below are buttons for controlling this servers privacy settings")
+ .setEmoji("NUCLEUS.LOGO")
+ .setStatus("Danger")
+ )
+ .setTitle("Options")
+ .setDescription("Options")
+ .setPageId(3)
+ .setComponents([
+ new ActionRowBuilder<ButtonBuilder>().addComponents([
+ new ButtonBuilder()
+ .setLabel("Clear all data")
+ .setCustomId("clear-all-data")
+ .setStyle(ButtonStyle.Danger)
+ ])
+ ])
+ ]
: []
);
const m = await interaction.reply({
@@ -150,14 +146,14 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
continue;
}
nextFooter = null;
- i.deferUpdate();
+ await i.deferUpdate();
if (i.customId === "left") {
if (page > 0) page--;
selectPaneOpen = false;
@@ -180,11 +176,12 @@
.setColor("Danger")
.send(true);
if (confirmation.cancelled) {
- break;
+ continue;
}
if (confirmation.success) {
client.database.guilds.delete(interaction.guild!.id);
client.database.history.delete(interaction.guild!.id);
+ client.database.notes.delete(interaction.guild!.id);
nextFooter = "All data cleared";
continue;
} else {
@@ -208,10 +205,6 @@
});
};
-const check = () => {
- return true;
-};
export { command };
export { callback };
-export { check };
diff --git a/src/commands/role/_meta.ts b/src/commands/role/_meta.ts
deleted file mode 100644
index f546d51..0000000
--- a/src/commands/role/_meta.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { command } from "../../utils/commandRegistration/slashCommandBuilder.js";
-
-const name = "role";
-const description = "Change roles for users";
-
-const subcommand = await command(name, description, `role`);
-
-export { name, description, subcommand as command };
diff --git a/src/commands/role/user.ts b/src/commands/role/user.ts
deleted file mode 100644
index ad29811..0000000
--- a/src/commands/role/user.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import type { CommandInteraction, GuildMember, Role, User } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-import client from "../../utils/client.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import keyValueList from "../../utils/generateKeyValueList.js";
-import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-
-const command = (builder: SlashCommandSubcommandBuilder) =>
- builder
- .setName("user")
- .setDescription("Gives or removes a role from someone")
- .addUserOption((option) =>
- option.setName("user").setDescription("The member to give or remove the role from").setRequired(true)
- )
- .addRoleOption((option) =>
- option.setName("role").setDescription("The role to give or remove").setRequired(true)
- )
- .addStringOption((option) =>
- option
- .setName("action")
- .setDescription("The action to perform")
- .setRequired(true)
- .addChoices(
- {name: "Add", value: "give"},
- {name: "Remove", value: "remove"}
- )
- );
-
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const { renderUser, renderRole } = client.logger;
- const action = interaction.options.get("action")?.value as string;
- const role: Role = (await interaction.guild!.roles.fetch(interaction.options.get("role")?.value as string))!;
- // TODO:[Modals] Replace this with a modal
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Role")
- .setDescription(
- keyValueList({
- user: renderUser(interaction.options.getUser("user")! as User),
- role: renderRole(role)
- }) +
- `\nAre you sure you want to ${
- action === "give" ? "give the role to" : "remove the role from"
- } ${interaction.options.getUser("user")}?`
- )
- .setColor("Danger")
- .setFailedMessage("No changes were made", "Success", "GUILD.ROLES.CREATE")
- .send();
- if (confirmation.cancelled || !confirmation.success) return;
- try {
- const member = interaction.options.getMember("user") as GuildMember;
- if ((interaction.options.get("action")?.value as string) === "give") {
- member.roles.add(role);
- } else {
- member.roles.remove(role);
- }
- } catch (e) {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Role")
- .setDescription("Something went wrong and the role could not be added")
- .setStatus("Danger")
- .setEmoji("CONTROL.BLOCKCROSS")
- ],
- components: []
- });
- }
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Role")
- .setDescription(`The role has been ${action === "give" ? "given" : "removed"} successfully`)
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: []
- });
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as GuildMember;
- if (!interaction.guild) return
- const me = interaction.guild.members.me!;
- const apply = interaction.options.getMember("user") as GuildMember | null;
- if (apply === null) return "That member is not in the server";
- // Check if Nucleus has permission to role
- if (!me.permissions.has("ManageRoles")) return "I do not have the *Manage Roles* permission";
- // Allow the owner to role anyone
- if (member.id === interaction.guild.ownerId) return true;
- // Check if the user has manage_roles permission
- if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
- // Allow role
- return true;
-};
-
-export { command };
-export { callback };
-export { check };
diff --git a/src/commands/rolemenu.ts b/src/commands/rolemenu.ts
index c1ceb2e..2861e05 100644
--- a/src/commands/rolemenu.ts
+++ b/src/commands/rolemenu.ts
@@ -1,5 +1,4 @@
-import type { CommandInteraction } from "discord.js";
-import { SlashCommandBuilder } from "@discordjs/builders";
+import { CommandInteraction, SlashCommandBuilder } from "discord.js";
import { callback as roleMenu } from "../actions/roleMenu.js";
const command = new SlashCommandBuilder()
@@ -10,10 +9,5 @@
await roleMenu(interaction);
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/server/about.ts b/src/commands/server/about.ts
index 23a53b7..4c88365 100644
--- a/src/commands/server/about.ts
+++ b/src/commands/server/about.ts
@@ -1,5 +1,5 @@
import { CommandInteraction, GuildMFALevel } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import generateKeyValueList, { toCapitals } from "../../utils/generateKeyValueList.js";
@@ -74,10 +74,5 @@
});
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/server/buttons.ts b/src/commands/server/buttons.ts
new file mode 100644
index 0000000..3297616
--- /dev/null
+++ b/src/commands/server/buttons.ts
@@ -0,0 +1,252 @@
+import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType, CommandInteraction, MessageCreateOptions, ModalBuilder, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
+import type Discord from "discord.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import lodash from "lodash";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+
+export const command = new SlashCommandSubcommandBuilder()
+ .setName("buttons")
+ .setDescription("Create clickable buttons for verifying, role menus etc.");
+
+interface Data {
+ buttons: string[],
+ title: string | null,
+ description: string | null,
+ color: number,
+ channel: string | null
+}
+
+const colors: Record<string, number> = {
+ RED: 0xF27878,
+ ORANGE: 0xE5AB71,
+ YELLOW: 0xF2D478,
+ GREEN: 0x65CC76,
+ BLUE: 0x72AEF5,
+ PURPLE: 0xA358B2,
+ PINK: 0xD46899,
+ GRAY: 0x999999,
+}
+
+const buttonNames: Record<string, string> = {
+ verifybutton: "Verify",
+ rolemenu: "Role Menu",
+ createticket: "Create Ticket"
+}
+
+export const callback = async (interaction: CommandInteraction): Promise<void> => {
+
+ const m = await interaction.reply({
+ embeds: LoadingEmbed,
+ fetchReply: true,
+ ephemeral: true
+ });
+
+ let closed = false;
+ const data: Data = {
+ buttons: [],
+ title: null,
+ description: null,
+ color: colors["RED"]!,
+ channel: interaction.channelId
+ }
+ do {
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("edit")
+ .setLabel("Edit Embed")
+ .setStyle(ButtonStyle.Secondary),
+ new ButtonBuilder()
+ .setCustomId("send")
+ .setLabel("Send")
+ .setStyle(ButtonStyle.Primary)
+ .setDisabled(!data.channel)
+ );
+
+ const colorSelect = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("color")
+ .setPlaceholder("Select a color")
+ .setMinValues(1)
+ .addOptions(
+ Object.keys(colors).map((color: string) => {
+ return new StringSelectMenuOptionBuilder()
+ .setLabel(lodash.capitalize(color))
+ .setValue(color)
+ .setEmoji(getEmojiByName("COLORS." + color, "id") as APIMessageComponentEmoji)
+ .setDefault(data.color === colors[color])
+ }
+ )
+ )
+ );
+
+ const buttonSelect = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("button")
+ .setPlaceholder("Select buttons to add")
+ .setMinValues(1)
+ .setMaxValues(3)
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Verify")
+ .setValue("verifybutton")
+ .setDescription("Click to get verified in the server")
+ .setDefault(data.buttons.includes("verifybutton")),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Role Menu")
+ .setValue("rolemenu")
+ .setDescription("Click to customize your roles")
+ .setDefault(data.buttons.includes("rolemenu")),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Ticket")
+ .setValue("createticket")
+ .setDescription("Click to create a support ticket")
+ .setDefault(data.buttons.includes("createticket"))
+ )
+ )
+
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText, ChannelType.GuildAnnouncement, ChannelType.PublicThread, ChannelType.AnnouncementThread)
+ )
+ let channelName = interaction.guild!.channels.cache.get(data.channel!)?.name;
+ if (data.channel === interaction.channelId) channelName = "this channel";
+ const embed = new EmojiEmbed()
+ .setTitle(data.title ?? "No title set")
+ .setDescription(data.description ?? "*No description set*")
+ .setColor(data.color)
+ .setFooter({text: `Click the button below to edit the embed | The embed will be sent in ${channelName}`});
+
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [colorSelect, buttonSelect, channelMenu, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction;
+ try {
+ i = await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ }) as Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction | Discord.StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "edit": {
+ await i.showModal(
+ new ModalBuilder()
+ .setCustomId("modal")
+ .setTitle(`Options for ${i.customId}`)
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("title")
+ .setLabel("Title")
+ .setMaxLength(256)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Short)
+ .setValue(data.title ?? "")
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("description")
+ .setLabel("The text to display below the title")
+ .setMaxLength(4000)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ .setValue(data.description ?? "")
+ )
+ )
+ );
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Button Editor")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("GUILD.TICKET.OPEN")
+ ],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents([
+ new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ ])
+ ]
+ });
+ let out: Discord.ModalSubmitInteraction | null;
+ try {
+ out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ if (!out || out.isButton()) continue
+ data.title = out.fields.getTextInputValue("title").length === 0 ? null : out.fields.getTextInputValue("title");
+ data.description = out.fields.getTextInputValue("description").length === 0 ? null : out.fields.getTextInputValue("description");
+ break;
+ }
+ case "send": {
+ await i.deferUpdate();
+ const channel = interaction.guild!.channels.cache.get(data.channel!) as Discord.TextChannel;
+ const components = new ActionRowBuilder<ButtonBuilder>();
+ for(const button of data.buttons) {
+ components.addComponents(
+ new ButtonBuilder()
+ .setCustomId(button)
+ .setLabel(buttonNames[button]!)
+ .setStyle(ButtonStyle.Primary)
+ );
+ }
+ const messageData: MessageCreateOptions = {components: [components]}
+ if (data.title || data.description) {
+ const e = new EmojiEmbed()
+ if(data.title) e.setTitle(data.title);
+ if(data.description) e.setDescription(data.description);
+ if(data.color) e.setColor(data.color);
+ messageData.embeds = [e];
+ }
+ await channel.send(messageData);
+ break;
+ }
+ }
+ } else if(i.isStringSelectMenu()) {
+ try {await i.deferUpdate();} catch (err) {console.log(err)}
+ switch(i.customId) {
+ case "color": {
+ data.color = colors[i.values[0]!]!;
+ break;
+ }
+ case "button": {
+ data.buttons = i.values;
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ data.channel = i.values[0]!;
+ }
+
+ } while (!closed);
+ await interaction.deleteReply();
+}
+
+export const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ if (!member.permissions.has("ManageMessages"))
+ return "You must have the *Manage Messages* permission to use this command";
+ return true;
+};
diff --git a/src/commands/settings/automod.ts b/src/commands/settings/automod.ts
new file mode 100644
index 0000000..09b8914
--- /dev/null
+++ b/src/commands/settings/automod.ts
@@ -0,0 +1,922 @@
+import type Discord from "discord.js";
+import { ActionRowBuilder,
+ AnySelectMenuInteraction,
+ APIMessageComponentEmoji,
+ ButtonBuilder,
+ ButtonInteraction,
+ ButtonStyle,
+ ChannelSelectMenuBuilder,
+ ChannelSelectMenuInteraction,
+ CommandInteraction,
+ Message,
+ ModalBuilder,
+ RoleSelectMenuBuilder,
+ RoleSelectMenuInteraction,
+ StringSelectMenuBuilder,
+ StringSelectMenuInteraction,
+ StringSelectMenuOptionBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+ UserSelectMenuBuilder,
+ UserSelectMenuInteraction
+} from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import client from "../../utils/client.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+import listToAndMore from "../../utils/listToAndMore.js";
+
+
+const command = (builder: SlashCommandSubcommandBuilder) =>
+ builder.setName("automod").setDescription("Setting for automatic moderation features");
+
+
+const emojiFromBoolean = (bool: boolean, id?: string) => bool ? getEmojiByName("CONTROL.TICK", id) : getEmojiByName("CONTROL.CROSS", id);
+
+const toSelectMenu = async (interaction: StringSelectMenuInteraction, m: Message, ids: string[], type: "member" | "role" | "channel", title: string): Promise<string[]> => {
+
+ const back = new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder().setCustomId("back").setLabel("Back").setStyle(ButtonStyle.Secondary).setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji));
+ let closed;
+ do {
+ let render: string[] = []
+ let mapped: string[] = [];
+ let menu: UserSelectMenuBuilder | RoleSelectMenuBuilder | ChannelSelectMenuBuilder;
+ switch(type) {
+ case "member": {
+ menu = new UserSelectMenuBuilder().setCustomId("user").setPlaceholder("Select users").setMaxValues(25);
+ mapped = await Promise.all(ids.map(async (id) => { return (await client.users.fetch(id).then(user => user.tag)) || "Unknown User" }));
+ render = ids.map(id => client.logger.renderUser(id))
+ break;
+ }
+ case "role": {
+ menu = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder("Select roles").setMaxValues(25);
+ mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.roles.fetch(id).then(role => role?.name ?? "Unknown Role"))}));
+ render = ids.map(id => client.logger.renderRole(id, interaction.guild!))
+ break;
+ }
+ case "channel": {
+ menu = new ChannelSelectMenuBuilder().setCustomId("channel").setPlaceholder("Select channels").setMaxValues(25);
+ mapped = await Promise.all(ids.map(async (id) => { return (await interaction.guild!.channels.fetch(id).then(channel => channel?.name ?? "Unknown Role")) }));
+ render = ids.map(id => client.logger.renderChannel(id))
+ break;
+ }
+ }
+ const removeOptions = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("remove")
+ .setPlaceholder("Remove")
+ .addOptions(mapped.map((name, i) => new StringSelectMenuOptionBuilder().setLabel(name).setValue(ids[i]!)))
+ .setDisabled(ids.length === 0)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle(title)
+ .setEmoji(getEmojiByName("GUILD.SETTINGS.GREEN"))
+ .setDescription(`Select ${type}s:\n\nCurrent:\n` + (render.length > 0 ? render.join("\n") : "None"))
+ .setStatus("Success");
+ const components: ActionRowBuilder<
+ StringSelectMenuBuilder |
+ ButtonBuilder |
+ ChannelSelectMenuBuilder |
+ UserSelectMenuBuilder |
+ RoleSelectMenuBuilder
+ >[] = [new ActionRowBuilder<typeof menu>().addComponents(menu)]
+ if(ids.length > 0) components.push(removeOptions);
+ components.push(back);
+
+ await interaction.editReply({embeds: [embed], components: components})
+
+ let i: AnySelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: i => i.user.id === interaction.user.id, time: 300000});
+ } catch(e) {
+ closed = true;
+ continue;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ if(i.customId === "back") {
+ closed = true;
+ break;
+ }
+ } else if(i.isStringSelectMenu()) {
+ await i.deferUpdate();
+ if(i.customId === "remove") {
+ ids = ids.filter(id => id !== (i as StringSelectMenuInteraction).values[0]);
+ if(ids.length === 0) {
+ menu.data.disabled = true;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ if(i.customId === "user") {
+ ids = ids.concat((i as UserSelectMenuInteraction).values);
+ } else if(i.customId === "role") {
+ ids = ids.concat((i as RoleSelectMenuInteraction).values);
+ } else if(i.customId === "channel") {
+ ids = ids.concat((i as ChannelSelectMenuInteraction).values);
+ }
+ }
+
+ } while(!closed)
+ return ids;
+}
+
+const imageMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ NSFW: boolean,
+ size: boolean
+}): Promise<{NSFW: boolean, size: boolean}> => {
+ let closed = false;
+ do {
+ const options = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("nsfw")
+ .setLabel("NSFW")
+ .setStyle(current.NSFW ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.NSFW, "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("size")
+ .setLabel("Size")
+ .setStyle(current.size ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.size, "id") as APIMessageComponentEmoji)
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Image Settings")
+ .setDescription(
+ `${emojiFromBoolean(current.NSFW)} **NSFW**\n` +
+ `${emojiFromBoolean(current.size)} **Size**\n`
+ )
+
+ await interaction.editReply({embeds: [embed], components: [options]});
+
+ let i: ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
+ } catch (e) {
+ return current;
+ }
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "nsfw": {
+ current.NSFW = !current.NSFW;
+ break;
+ }
+ case "size": {
+ current.size = !current.size;
+ break;
+ }
+ }
+ } while(!closed);
+ return current;
+}
+
+const wordMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ enabled: boolean,
+ words: {strict: string[], loose: string[]},
+ allowed: {users: string[], roles: string[], channels: string[]}
+}): Promise<{
+ enabled: boolean,
+ words: {strict: string[], loose: string[]},
+ allowed: {users: string[], roles: string[], channels: string[]}
+}> => {
+ let closed = false;
+ do {
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("enabled")
+ .setLabel("Enabled")
+ .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji),
+ );
+
+ const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("edit")
+ .setPlaceholder("Edit... ")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Words")
+ .setDescription("Edit your list of words to filter")
+ .setValue("words"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Allowed Users")
+ .setDescription("Users who will be unaffected by the word filter")
+ .setValue("allowedUsers"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Allowed Roles")
+ .setDescription("Roles that will be unaffected by the word filter")
+ .setValue("allowedRoles"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Allowed Channels")
+ .setDescription("Channels where the word filter will not apply")
+ .setValue("allowedChannels")
+ )
+ .setDisabled(!current.enabled)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Word Filters")
+ .setDescription(
+ `${emojiFromBoolean(current.enabled)} **Enabled**\n` +
+ `**Strict Words:** ${listToAndMore(current.words.strict, 5)}\n` +
+ `**Loose Words:** ${listToAndMore(current.words.loose, 5)}\n\n` +
+ `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` +
+ `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` +
+ `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5)
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+ await interaction.editReply({embeds: [embed], components: [selectMenu, buttons]});
+
+ let i: ButtonInteraction | StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "enabled": {
+ current.enabled = !current.enabled;
+ break;
+ }
+ }
+ } else {
+ switch(i.values[0]) {
+ case "words": {
+ await interaction.editReply({embeds: [new EmojiEmbed()
+ .setTitle("Word Filter")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )]})
+ const modal = new ModalBuilder()
+ .setTitle("Word Filter")
+ .setCustomId("wordFilter")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("wordStrict")
+ .setLabel("Strict Words")
+ .setPlaceholder("Matches anywhere in the message, including surrounded by other characters")
+ .setValue(current.words.strict.join(", "))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(false)
+ ),
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("wordLoose")
+ .setLabel("Loose Words")
+ .setPlaceholder("Matches only if the word is by itself, surrounded by spaces or punctuation")
+ .setValue(current.words.loose.join(", "))
+ .setStyle(TextInputStyle.Paragraph)
+ .setRequired(false)
+ )
+ )
+
+ await i.showModal(modal);
+ let out;
+ try {
+ out = await modalInteractionCollector(m, interaction.user);
+ } catch (e) {
+ break;
+ }
+ if (!out) break;
+ if(out.isButton()) break;
+ current.words.strict = out.fields.getTextInputValue("wordStrict")
+ .split(",").map(s => s.trim()).filter(s => s.length > 0);
+ current.words.loose = out.fields.getTextInputValue("wordLoose")
+ .split(",").map(s => s.trim()).filter(s => s.length > 0);
+ break;
+ }
+ case "allowedUsers": {
+ await i.deferUpdate();
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Word Filter");
+ break;
+ }
+ case "allowedRoles": {
+ await i.deferUpdate();
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Word Filter");
+ break;
+ }
+ case "allowedChannels": {
+ await i.deferUpdate();
+ current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Word Filter");
+ break;
+ }
+ }
+ }
+ } while(!closed);
+ return current;
+}
+
+const inviteMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ enabled: boolean,
+ allowed: {users: string[], roles: string[], channels: string[]}
+}): Promise<{
+ enabled: boolean,
+ allowed: {users: string[], roles: string[], channels: string[]}
+}> => {
+
+ let closed = false;
+ do {
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("enabled")
+ .setLabel(current.enabled ? "Enabled" : "Disabled")
+ .setStyle(current.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.enabled, "id") as APIMessageComponentEmoji)
+ );
+ const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("toEdit")
+ .setPlaceholder("Edit your allow list")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Users")
+ .setDescription("Users that are allowed to send invites")
+ .setValue("users"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are allowed to send invites")
+ .setValue("roles"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Channels")
+ .setDescription("Channels that anyone is allowed to send invites in")
+ .setValue("channels")
+ ).setDisabled(!current.enabled)
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Invite Settings")
+ .setDescription(
+ "Automatically deletes invites sent by users (outside of staff members and self promotion channels)" + `\n\n` +
+ `${emojiFromBoolean(current.enabled)} **${current.enabled ? "Enabled" : "Disabled"}**\n\n` +
+ `**Users:** ` + listToAndMore(current.allowed.users.map(user => `<@${user}>`), 5) + `\n` +
+ `**Roles:** ` + listToAndMore(current.allowed.roles.map(role => `<@&${role}>`), 5) + `\n` +
+ `**Channels:** ` + listToAndMore(current.allowed.channels.map(channel => `<#${channel}>`), 5)
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+
+ await interaction.editReply({embeds: [embed], components: [menu, buttons]});
+
+ let i: ButtonInteraction | StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ return current;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "enabled": {
+ current.enabled = !current.enabled;
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ switch(i.values[0]) {
+ case "users": {
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Invite Settings");
+ break;
+ }
+ case "roles": {
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Invite Settings");
+ break;
+ }
+ case "channels": {
+ current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Invite Settings");
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ return current;
+}
+
+const mentionMenu = async (interaction: StringSelectMenuInteraction, m: Message, current: {
+ mass: number,
+ everyone: boolean,
+ roles: boolean,
+ allowed: {
+ roles: string[],
+ rolesToMention: string[],
+ users: string[],
+ channels: string[]
+ }
+}): Promise<{
+ mass: number,
+ everyone: boolean,
+ roles: boolean,
+ allowed: {
+ roles: string[],
+ rolesToMention: string[],
+ users: string[],
+ channels: string[]
+ }
+}> => {
+ let closed = false;
+
+ do {
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("everyone")
+ .setLabel(current.everyone ? "Everyone" : "No one")
+ .setStyle(current.everyone ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.everyone, "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("roles")
+ .setLabel(current.roles ? "Roles" : "No roles")
+ .setStyle(current.roles ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(emojiFromBoolean(current.roles, "id") as APIMessageComponentEmoji)
+ );
+ const menu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("toEdit")
+ .setPlaceholder("Edit mention settings")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Mass Mention Amount")
+ .setDescription("The amount of mentions before the bot will delete the message")
+ .setValue("mass"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are able to be mentioned")
+ .setValue("roles"),
+ )
+ )
+
+ const allowedMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("allowed")
+ .setPlaceholder("Edit exceptions")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Users")
+ .setDescription("Users that are unaffected by the mention filter")
+ .setValue("users"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are unaffected by the mention filter")
+ .setValue("roles"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Channels")
+ .setDescription("Channels where anyone is unaffected by the mention filter")
+ .setValue("channels")
+ )
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Mention Settings")
+ .setDescription(
+ `Log when members mention:\n` +
+ `${emojiFromBoolean(true)} **${current.mass}+ members** in one message\n` +
+ `${emojiFromBoolean(current.everyone)} **Everyone**\n` +
+ `${emojiFromBoolean(current.roles)} **Roles**\n` +
+ (current.allowed.rolesToMention.length > 0 ? `> *Except for ${listToAndMore(current.allowed.rolesToMention.map(r => `<@&${r}>`), 3)}*\n` : "") +
+ "\n" +
+ `Except if...\n` +
+ `> ${current.allowed.users.length > 0 ? `Member is: ${listToAndMore(current.allowed.users.map(u => `<@${u}>`), 3)}\n` : ""}` +
+ `> ${current.allowed.roles.length > 0 ? `Member has role: ${listToAndMore(current.allowed.roles.map(r => `<@&${r}>`), 3)}\n` : ""}` +
+ `> ${current.allowed.channels.length > 0 ? `In channel: ${listToAndMore(current.allowed.channels.map(c => `<#${c}>`), 3)}\n` : ""}`
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+ await interaction.editReply({embeds: [embed], components: [menu, allowedMenu, buttons]});
+
+ let i: ButtonInteraction | StringSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+
+ if(i.isButton()) {
+ await i.deferUpdate();
+ switch (i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ case "everyone": {
+ current.everyone = !current.everyone;
+ break;
+ }
+ case "roles": {
+ current.roles = !current.roles;
+ break;
+ }
+ }
+ } else {
+ switch (i.customId) {
+ case "toEdit": {
+ switch (i.values[0]) {
+ case "mass": {
+ await interaction.editReply({embeds: [new EmojiEmbed()
+ .setTitle("Word Filter")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ ], components: [new ActionRowBuilder<ButtonBuilder>().addComponents(new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )]})
+ const modal = new ModalBuilder()
+ .setTitle("Mass Mention Amount")
+ .setCustomId("mass")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("mass")
+ .setPlaceholder("Amount")
+ .setMinLength(1)
+ .setMaxLength(3)
+ .setStyle(TextInputStyle.Short)
+ )
+ )
+ await i.showModal(modal);
+ let out;
+ try {
+ out = await modalInteractionCollector(m, interaction.user);
+ } catch (e) {
+ break;
+ }
+ if (!out) break;
+ if(out.isButton()) break;
+ current.mass = parseInt(out.fields.getTextInputValue("mass"));
+ break;
+ }
+ case "roles": {
+ await i.deferUpdate();
+ current.allowed.rolesToMention = await toSelectMenu(interaction, m, current.allowed.rolesToMention, "role", "Mention Settings");
+ break;
+ }
+ }
+ break;
+ }
+ case "allowed": {
+ await i.deferUpdate();
+ switch (i.values[0]) {
+ case "users": {
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings");
+ break;
+ }
+ case "roles": {
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings");
+ break;
+ }
+ case "channels": {
+ current.allowed.channels = await toSelectMenu(interaction, m, current.allowed.channels, "channel", "Mention Settings");
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ return current
+}
+
+const cleanMenu = async (interaction: StringSelectMenuInteraction, m: Message, current?: {
+ channels?: string[],
+ allowed?: {
+ roles: string[],
+ users: string[]
+ }
+}): Promise<{
+ channels: string[],
+ allowed: {
+ roles: string[],
+ users: string[]
+ }
+}> => {
+ let closed = false;
+ if(!current) current = {channels: [], allowed: {roles: [], users: []}};
+ if(!current.channels) current.channels = [];
+ if(!current.allowed) current.allowed = {roles: [], users: []};
+
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("toAdd")
+ .setPlaceholder("Select a channel")
+ )
+
+ const allowedMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("allowed")
+ .setPlaceholder("Edit exceptions")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Users")
+ .setDescription("Users that are unaffected by the mention filter")
+ .setValue("users"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Roles")
+ .setDescription("Roles that are unaffected by the mention filter")
+ .setValue("roles")
+ )
+ )
+
+ do {
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ )
+
+ const embed = new EmojiEmbed()
+ .setTitle("Clean Settings")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ .setDescription(
+ `Current clean channels:\n\n` +
+ `${current.channels.length > 0 ? listToAndMore(current.channels.map(c => `<#${c}>`), 10) : "None"}\n\n`
+ )
+ .setStatus("Success")
+
+
+ await interaction.editReply({embeds: [embed], components: [channelMenu, allowedMenu, buttons]});
+
+ let i: ButtonInteraction | ChannelSelectMenuInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction | ChannelSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ closed = true;
+ break;
+ }
+ }
+ } else {
+ switch (i.customId) {
+ case "toAdd": {
+ const channelEmbed = new EmojiEmbed()
+ .setTitle("Clean Settings")
+ .setDescription(`Editing <#${i.values[0]}>`)
+ .setEmoji("GUILD.SETTINGS.GREEN")
+ .setStatus("Success")
+ const channelButtons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id")),
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(current.channels.includes(i.values[0]!) ? "Remove" : "Add")
+ .setStyle(current.channels.includes(i.values[0]!) ? ButtonStyle.Danger : ButtonStyle.Success)
+ )
+
+ await i.editReply({embeds: [channelEmbed], components: [channelButtons]});
+ let j: ButtonInteraction;
+ try {
+ j = await m.awaitMessageComponent({filter: (i) => interaction.user.id === i.user.id && i.message.id === m.id, time: 300000}) as ButtonInteraction;
+ } catch (e) {
+ closed = true;
+ break;
+ }
+ await j.deferUpdate();
+ switch (j.customId) {
+ case "back": {
+ break;
+ }
+ case "switch": {
+ if(current.channels.includes(i.values[0]!)) {
+ current.channels.splice(current.channels.indexOf(i.values[0]!), 1);
+ } else {
+ current.channels.push(i.values[0]!);
+ }
+ }
+ }
+ break;
+ }
+ case "allowed": {
+ switch (i.values[0]) {
+ case "users": {
+ current.allowed.users = await toSelectMenu(interaction, m, current.allowed.users, "member", "Mention Settings");
+ break;
+ }
+ case "roles": {
+ current.allowed.roles = await toSelectMenu(interaction, m, current.allowed.roles, "role", "Mention Settings");
+ break;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+
+ return current as {
+ channels: string[],
+ allowed: {
+ roles: string[],
+ users: string[]
+ }
+ };
+
+}
+
+const callback = async (interaction: CommandInteraction): Promise<void> => {
+ if (!interaction.guild) return;
+ const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true});
+ const config = (await client.database.guilds.read(interaction.guild.id)).filters;
+
+ let closed = false;
+
+ const button = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ )
+
+ do {
+
+ const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("filter")
+ .setPlaceholder("Select a filter to edit")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Invites")
+ .setDescription("Automatically delete messages containing server invites")
+ .setValue("invites"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Mentions")
+ .setDescription("Deletes messages with excessive mentions")
+ .setValue("mentions"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Words")
+ .setDescription("Delete messages containing filtered words")
+ .setValue("words"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Malware")
+ .setDescription("Automatically delete files and links containing malware")
+ .setValue("malware"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Images")
+ .setDescription("Checks performed on images (NSFW, size checking, etc.)")
+ .setValue("images"),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Clean")
+ .setDescription("Automatically delete new messages in specific channels")
+ .setValue("clean")
+ )
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Automod Settings")
+ .setDescription(
+ `${emojiFromBoolean(config.invite.enabled)} **Invites**\n` +
+ `${emojiFromBoolean(config.pings.everyone || config.pings.mass > 0 || config.pings.roles)} **Mentions**\n` +
+ `${emojiFromBoolean(config.wordFilter.enabled)} **Words**\n` +
+ `${emojiFromBoolean(config.malware)} **Malware**\n` +
+ `${emojiFromBoolean(config.images.NSFW || config.images.size)} **Images**\n` +
+ `${emojiFromBoolean(config.clean.channels.length > 0)} **Clean**\n`
+ )
+ .setStatus("Success")
+ .setEmoji("GUILD.SETTINGS.GREEN")
+
+
+ await interaction.editReply({embeds: [embed], components: [selectMenu, button]});
+
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id, time: 300000}) as StringSelectMenuInteraction | ButtonInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ await client.database.guilds.write(interaction.guild.id, {filters: config});
+ } else {
+ switch(i.values[0]) {
+ case "invites": {
+ config.invite = await inviteMenu(i, m, config.invite);
+ break;
+ }
+ case "mentions": {
+ config.pings = await mentionMenu(i, m, config.pings);
+ break;
+ }
+ case "words": {
+ config.wordFilter = await wordMenu(i, m, config.wordFilter);
+ break;
+ }
+ case "malware": {
+ config.malware = !config.malware;
+ break;
+ }
+ case "images": {
+ const next = await imageMenu(i, m, config.images);
+ config.images = next;
+ break;
+ }
+ case "clean": {
+ const next = await cleanMenu(i, m, config.clean);
+ config.clean = next;
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ await interaction.deleteReply()
+
+};
+
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ if (!member.permissions.has("ManageMessages"))
+ return "You must have the *Manage Messages* permission to use this command";
+ return true;
+};
+
+export { command };
+export { callback };
+export { check };
diff --git a/src/commands/settings/autopublish.ts b/src/commands/settings/autopublish.ts
new file mode 100644
index 0000000..1dc97e0
--- /dev/null
+++ b/src/commands/settings/autopublish.ts
@@ -0,0 +1,96 @@
+import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, CommandInteraction, SlashCommandSubcommandBuilder } from "discord.js";
+import type Discord from "discord.js";
+import client from "../../utils/client.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import compare from "lodash"
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+
+export const command = new SlashCommandSubcommandBuilder()
+ .setName("autopublish")
+ .setDescription("Automatically publish messages posted in announcement channels");
+
+export const callback = async (interaction: CommandInteraction): Promise<void> => {
+ await interaction.reply({
+ embeds: LoadingEmbed,
+ ephemeral: true,
+ fetchReply: true
+ });
+
+ let closed = false;
+ let config = await client.database.guilds.read(interaction.guild!.id);
+ let data = Object.assign({}, config.autoPublish);
+ do {
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Disabled" : "Enabled")
+ .setStyle(data.enabled ? ButtonStyle.Danger : ButtonStyle.Success)
+ .setEmoji(data.enabled ? "✅" : "❌"),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji("💾")
+ .setDisabled(compare.isEqual(data, config.autoPublish))
+ );
+
+ const channelSelect = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setMinValues(1)
+ );
+
+ const embed = new EmojiEmbed()
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [channelSelect, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction;
+ try {
+ i = await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ }) as Discord.ButtonInteraction | Discord.ChannelSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "switch": {
+ data.enabled = !data.enabled;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, { "autoPublish": data });
+ config = await client.database.guilds.read(interaction.guild!.id);
+ data = Object.assign({}, config.autoPublish);
+ break;
+ }
+ }
+ } else {
+ for(const channel of i.values) {
+ data.channels.includes(channel) ? data.channels.splice(data.channels.indexOf(channel), 1) : data.channels.push(channel);
+ }
+ }
+
+ } while (!closed);
+
+ await interaction.deleteReply();
+}
+
+export const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ const me = interaction.guild!.members.me!;
+ if (!member.permissions.has("ManageMessages"))
+ return "You must have the *Manage Messages* permission to use this command";
+ if (_partial) return true;
+ if (!me.permissions.has("ManageMessages")) return "I do not have the *Manage Messages* permission";
+ return true;
+};
diff --git a/src/commands/settings/filters.ts b/src/commands/settings/filters.ts
deleted file mode 100644
index 2e6f4c5..0000000
--- a/src/commands/settings/filters.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type Discord from "discord.js";
-import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-
-const command = (builder: SlashCommandSubcommandBuilder) =>
- builder.setName("filter").setDescription("Setting for message filters");
-
-const callback = async (_interaction: CommandInteraction): Promise<void> => {
- console.log("Filters");
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as Discord.GuildMember;
- if (!member.permissions.has("ManageMessages"))
- return "You must have the *Manage Messages* permission to use this command";
- return true;
-};
-
-export { command };
-export { callback };
-export { check };
diff --git a/src/commands/settings/logs/attachment.ts b/src/commands/settings/logs/attachment.ts
index 2709bee..238b8b9 100644
--- a/src/commands/settings/logs/attachment.ts
+++ b/src/commands/settings/logs/attachment.ts
@@ -1,198 +1,110 @@
import { LoadingEmbed } from "../../../utils/defaults.js";
-import { ChannelType } from "discord-api-types/v9";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction } from "discord.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType } from "discord.js";
import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../../utils/confirmationMessage.js";
import getEmojiByName from "../../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../../utils/client.js";
+import { getCommandMentionByName } from "../../../utils/getCommandDataByName.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("attachments")
.setDescription("Where attachments should be logged to (Premium only)")
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel to log attachments in")
- .addChannelTypes(ChannelType.GuildText)
- .setRequired(false)
- );
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const m = (await interaction.reply({
+ if (interaction.guild) client.database.premium.hasPremium(interaction.guild.id).finally(() => {});
+ await interaction.reply({
embeds: LoadingEmbed,
ephemeral: true,
fetchReply: true
- })) as Discord.Message;
- if (interaction.options.get("channel")?.channel) {
- let channel;
- try {
- channel = interaction.options.get("channel")?.channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Attachment Log 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("Attachment Log Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Attachment Log Channel")
- .setDescription(
- "This will be the channel all attachments will be sent to.\n\n" +
- `Are you sure you want to set the attachment log channel to <#${channel.id}>?`
- )
- .setColor("Warning")
- .setFailedMessage("Attachment log channel not set", "Warning", "CHANNEL.TEXT.DELETE")
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.attachments.channel": channel.id
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "attachmentChannelUpdate",
- displayName: "Attachment Log 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 (e) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription("Something went wrong and the attachment log channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild!.id);
- let channel = data.logging.staff.channel;
+ })
- let timedOut = false;
- while (!timedOut) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription(
- channel
- ? `Your attachment log channel is currently set to <#${channel}>`
- : "This server does not have an attachment log channel" +
- (await client.database.premium.hasPremium(interaction.guild!.id)
- ? ""
- : "\n\nThis server does not have premium, so this feature is disabled")
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset channel")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!channel)
- ])
- ]
- });
- let i;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- timedOut = true;
- continue;
- }
- i.deferUpdate();
- if ((i.component as unknown as ButtonInteraction).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild!.id, null, ["logging.announcements.channel"]);
- channel = null;
- }
- }
- }
- await interaction.editReply({
+ if(!await client.database.premium.hasPremium(interaction.guild!.id)) return interaction.editReply({
embeds: [
new EmojiEmbed()
- .setTitle("Attachment Log Channel")
- .setDescription(
- channel
- ? `Your attachment log channel is currently set to <#${channel}>`
- : "This server does not have an attachment log channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- .setFooter({ text: "Message closed" })
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
+ .setTitle("Premium Required")
+ .setDescription(`This feature is exclusive to ${getCommandMentionByName("nucleus/premium")} servers.`)
+ .setStatus("Danger")
+ .setEmoji("NUCLEUS.PREMIUM")
+ ]
+ });
+
+ let data = await client.database.guilds.read(interaction.guild!.id);
+ let channel = data.logging.staff.channel;
+
+ let closed = false;
+ do {
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText)
+ );
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
new ButtonBuilder()
.setCustomId("clear")
.setLabel("Clear")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Secondary)
- .setDisabled(true)
- ])
- ]
- });
+ .setStyle(ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(!channel),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(channel === data.logging.staff.channel)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Attachments")
+ .setDescription(
+ `The channel to send all attachments from the server, allowing you to check them if they are deleted\n` +
+ `**Channel:** ${channel ? `<#${channel}>` : "*None*"}\n`
+ )
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE")
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [channelMenu, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ try {
+ i = (await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "clear": {
+ channel = null;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, {
+ "logging.attachments.channel": channel
+ });
+ data = await client.database.guilds.read(interaction.guild!.id);
+ break;
+ }
+ }
+ } else {
+ channel = i.values[0]!;
+ }
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/logs/channel.ts b/src/commands/settings/logs/channel.ts
deleted file mode 100644
index 992491a..0000000
--- a/src/commands/settings/logs/channel.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import { LoadingEmbed } from "../../../utils/defaults.js";
-import { ChannelType } from "discord-api-types/v9";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonInteraction, ButtonComponent } from "discord.js";
-import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../../utils/confirmationMessage.js";
-import getEmojiByName from "../../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-import client from "../../../utils/client.js";
-
-const command = (builder: SlashCommandSubcommandBuilder) =>
- builder
- .setName("channel")
- .setDescription("Sets or shows the log channel")
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel to set the log channel to")
- .addChannelTypes(ChannelType.GuildText)
- );
-
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const m = (await interaction.reply({
- embeds: LoadingEmbed,
- ephemeral: true,
- fetchReply: true
- })) as Discord.Message;
- if (interaction.options.get("channel")?.channel) {
- let channel;
- try {
- channel = interaction.options.get("channel")?.channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Log 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("Log Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Log Channel")
- .setDescription(`Are you sure you want to set the log channel to <#${channel.id}>?`)
- .setColor("Warning")
- .setFailedMessage("The log channel was not changed", "Danger", "CHANNEL.TEXT.DELETE")
- .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
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "logChannelUpdate",
- displayName: "Log Channel Changed",
- 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: channel.guild.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log Channel")
- .setDescription("Something went wrong and the log channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild!.id);
- let channel = data.logging.logs.channel;
- let timedOut = false;
- while (!timedOut) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log channel")
- .setDescription(
- channel
- ? `Your log channel is currently set to <#${channel}>`
- : "This server does not have a log channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset channel")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!channel)
- ])
- ]
- });
- let i: ButtonInteraction;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- }) as ButtonInteraction;
- } catch (e) {
- timedOut = true;
- }
- i = i!
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild!.id, null, ["logging.logs.channel"]);
- channel = null;
- }
- }
- }
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Log channel")
- .setDescription(
- channel
- ? `Your log channel is currently set to <#${channel}>`
- : "This server does not have a log channel"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- .setFooter({ text: "Message closed" })
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel("Clear")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Secondary)
- .setDisabled(true)
- ])
- ]
- });
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as Discord.GuildMember;
- if (!member.permissions.has("ManageGuild"))
- return "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/logs/events.ts b/src/commands/settings/logs/events.ts
index fbe79fa..eeef8fb 100644
--- a/src/commands/settings/logs/events.ts
+++ b/src/commands/settings/logs/events.ts
@@ -1,9 +1,11 @@
import { LoadingEmbed } from "../../../utils/defaults.js";
-import Discord, { CommandInteraction, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, EmbedBuilder, StringSelectMenuInteraction } from "discord.js";
-import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders";
-import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ChannelSelectMenuBuilder, ChannelType, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, ButtonInteraction, StringSelectMenuInteraction, ChannelSelectMenuInteraction, APIMessageComponentEmoji } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../../utils/client.js";
+import compare from "lodash";
import { toHexArray, toHexInteger } from "../../../utils/calculate.js";
+import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
+import getEmojiByName from "../../../utils/getEmojiByName.js";
const logs: Record<string, string> = {
channelUpdate: "Channels created, deleted or modified",
@@ -24,88 +26,138 @@
webhookUpdate: "Webhooks created or deleted",
guildMemberVerify: "Member runs verify",
autoModeratorDeleted: "Messages auto deleted by Nucleus",
- nucleusSettingsUpdated: "Nucleus' settings updated by a moderator",
- ticketUpdate: "Tickets created or deleted"
+ ticketUpdate: "Tickets created or deleted",
+ //nucleusSettingsUpdated: "Nucleus' settings updated by a moderator" // TODO
};
const command = (builder: SlashCommandSubcommandBuilder) =>
- builder.setName("events").setDescription("Sets what events should be logged");
+ builder
+ .setName("events")
+ .setDescription("The general log channel for the server, and setting what events to show")
const callback = async (interaction: CommandInteraction): Promise<void> => {
- await interaction.reply({
+ const m = (await interaction.reply({
embeds: LoadingEmbed,
- fetchReply: true,
- ephemeral: true
- });
- let m: Message;
- let timedOut = false;
+ ephemeral: true,
+ fetchReply: true
+ })) as Discord.Message;
+
+ let config = await client.database.guilds.read(interaction.guild!.id);
+ let data = Object.assign({}, config.logging.logs);
+ let closed = false;
+ let show = false;
do {
- const config = await client.database.guilds.read(interaction.guild!.id);
- const converted = toHexArray(config.logging.logs.toLog);
- const selectPane = new StringSelectMenuBuilder()
- .setPlaceholder("Set events to log")
- .setMaxValues(Object.keys(logs).length)
- .setCustomId("logs")
- .setMinValues(0)
- Object.keys(logs).map((e, i) => {
- selectPane.addOptions(new StringSelectMenuOptionBuilder()
- .setLabel(logs[e]!)
- .setValue(i.toString())
- .setDefault(converted.includes(e))
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText)
+ )
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Enabled" : "Disabled")
+ .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName((data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS"), "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("remove")
+ .setLabel("Remove")
+ .setStyle(ButtonStyle.Danger)
+ .setDisabled(!data.channel),
+ new ButtonBuilder()
+ .setCustomId("show")
+ .setLabel("Manage Events")
+ .setStyle(ButtonStyle.Primary),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setDisabled(compare.isEqual(data, config.logging.logs))
+ )
+
+ const converted = toHexArray(data.toLog);
+ const toLogMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setPlaceholder("Set events to log")
+ .setMaxValues(Object.keys(logs).length)
+ .setCustomId("logs")
+ .setMinValues(0)
+ )
+ Object.keys(logs).map((e) => {
+ toLogMenu.components[0]!.addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel(logs[e]!)
+ .setValue(e)
+ .setDefault(converted.includes(e))
)
});
- m = (await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Logging Events")
- .setDescription(
- "Below are the events being logged in the server. You can toggle them on and off in the dropdown"
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(selectPane),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder().setLabel("Select all").setStyle(ButtonStyle.Primary).setCustomId("all"),
- new ButtonBuilder().setLabel("Select none").setStyle(ButtonStyle.Danger).setCustomId("none")
- ])
- ]
- })) as Message;
- let i;
+
+ const embed = new EmojiEmbed()
+ .setTitle("General Log Channel")
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE")
+ .setDescription(
+ `This is the channel that all events you set to be logged will be stored\n` +
+ `**Channel:** ${data.channel ? `<#${data.channel}>` : "None"}\n`
+ )
+
+ const components: ActionRowBuilder<ButtonBuilder | ChannelSelectMenuBuilder | StringSelectMenuBuilder>[] = [channelMenu, buttons];
+ if(show) components.push(toLogMenu);
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: components
+ });
+
+ let i: ButtonInteraction | StringSelectMenuInteraction | ChannelSelectMenuInteraction;
try {
i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
+ filter: (i) => i.user.id === interaction.user.id,
+ time: 300000
+ }) as ButtonInteraction | StringSelectMenuInteraction | ChannelSelectMenuInteraction;
} catch (e) {
- timedOut = true;
+ closed = true;
continue;
}
- i.deferUpdate();
- if (i.customId === "logs") {
- const selected = (i as StringSelectMenuInteraction).values;
- const newLogs = toHexInteger(selected.map((e: string) => Object.keys(logs)[parseInt(e)]!));
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.toLog": newLogs
- });
- } else if (i.customId === "all") {
- const newLogs = toHexInteger(Object.keys(logs).map((e) => e));
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.toLog": newLogs
- });
- } else if (i.customId === "none") {
- await client.database.guilds.write(interaction.guild!.id, {
- "logging.logs.toLog": 0
- });
- }
- } while (!timedOut);
- await interaction.editReply({ embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })] });
- return;
+ await i.deferUpdate();
+
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "show": {
+ show = !show;
+ break;
+ }
+ case "switch": {
+ data.enabled = !data.enabled;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, {"logging.logs": data});
+ config = await client.database.guilds.read(interaction.guild!.id);
+ data = Object.assign({}, config.logging.logs);
+ break;
+ }
+ case "remove": {
+ data.channel = null;
+ break;
+ }
+ }
+ } else if(i.isStringSelectMenu()) {
+ const hex = toHexInteger(i.values);
+ data.toLog = hex;
+ } else if(i.isChannelSelectMenu()) {
+ data.channel = i.values[0]!;
+ }
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/logs/staff.ts b/src/commands/settings/logs/staff.ts
deleted file mode 100644
index 13125ef..0000000
--- a/src/commands/settings/logs/staff.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-import { LoadingEmbed } from "../../../utils/defaults.js";
-import { ChannelType } from "discord-api-types/v9";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonComponent } from "discord.js";
-import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../../utils/confirmationMessage.js";
-import getEmojiByName from "../../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
-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.GuildText)
- .setRequired(false)
- );
-
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- if (!interaction.guild) return;
- const m = (await interaction.reply({
- embeds: LoadingEmbed,
- ephemeral: true,
- fetchReply: true
- })) as Discord.Message;
- if (interaction.options.get("channel")?.channel) {
- let channel;
- try {
- channel = interaction.options.get("channel")?.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")
- ]
- });
- }
- const 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")
- .setFailedMessage("Staff notifications channel not set", "Warning", "CHANNEL.TEXT.DELETE")
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- 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;
- const data = {
- meta: {
- type: "staffChannelUpdate",
- 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 (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;
- const data = await client.database.guilds.read(interaction.guild.id);
- let channel = data.logging.staff.channel;
- let timedOut = false;
- while (!timedOut) {
- 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 ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset channel")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!channel)
- ])
- ]
- });
- let i;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- timedOut = true;
- continue;
- }
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild.id, null, ["logging.staff.channel"]);
- channel = null;
- }
- }
- }
- 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 ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel("Clear")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Secondary)
- .setDisabled(true)
- ])
- ]
- });
-};
-
-const check = (interaction: CommandInteraction) => {
- const member = interaction.member as Discord.GuildMember;
- if (!member.permissions.has("ManageGuild"))
- return "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/logs/warnings.ts b/src/commands/settings/logs/warnings.ts
new file mode 100644
index 0000000..84772e6
--- /dev/null
+++ b/src/commands/settings/logs/warnings.ts
@@ -0,0 +1,104 @@
+import { LoadingEmbed } from "../../../utils/defaults.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelSelectMenuBuilder, ChannelType } from "discord.js";
+import EmojiEmbed from "../../../utils/generateEmojiEmbed.js";
+import getEmojiByName from "../../../utils/getEmojiByName.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
+import client from "../../../utils/client.js";
+
+const command = (builder: SlashCommandSubcommandBuilder) =>
+ builder
+ .setName("warnings")
+ .setDescription("Settings for the staff notifications channel")
+
+const callback = async (interaction: CommandInteraction): Promise<unknown> => {
+ if (!interaction.guild) return;
+ await interaction.reply({
+ embeds: LoadingEmbed,
+ ephemeral: true,
+ fetchReply: true
+ })
+
+ let data = await client.database.guilds.read(interaction.guild.id);
+ let channel = data.logging.staff.channel;
+ let closed = false;
+ do {
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel")
+ .setChannelTypes(ChannelType.GuildText)
+ );
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("clear")
+ .setLabel("Clear")
+ .setStyle(ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(!channel),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as Discord.APIMessageComponentEmoji)
+ .setDisabled(channel === data.logging.staff.channel)
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Staff Notifications Channel")
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE")
+ .setDescription(
+ `Logs which require an action from a moderator or administrator will be sent to this channel.\n` +
+ `**Channel:** ${channel ? `<#${channel}>` : "*None*"}\n`
+ )
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [channelMenu, buttons]
+ });
+
+ let i: Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ try {
+ i = (await interaction.channel!.awaitMessageComponent({
+ filter: (i: Discord.Interaction) => i.user.id === interaction.user.id,
+ time: 300000
+ })) as Discord.ButtonInteraction | Discord.SelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+ await i.deferUpdate();
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "clear": {
+ channel = null;
+ break;
+ }
+ case "save": {
+ await client.database.guilds.write(interaction.guild!.id, {
+ "logging.warnings.channel": channel
+ });
+ data = await client.database.guilds.read(interaction.guild!.id);
+ break;
+ }
+ }
+ } else {
+ channel = i.values[0]!;
+ }
+ } while (!closed);
+
+ await interaction.deleteReply()
+};
+
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as Discord.GuildMember;
+ if (!member.permissions.has("ManageGuild"))
+ return "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/commands.ts b/src/commands/settings/moderation.ts
similarity index 73%
rename from src/commands/settings/commands.ts
rename to src/commands/settings/moderation.ts
index 25034b2..336e53a 100644
--- a/src/commands/settings/commands.ts
+++ b/src/commands/settings/moderation.ts
@@ -1,50 +1,28 @@
import { LoadingEmbed } from "../../utils/defaults.js";
-import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, Role, ButtonStyle, ButtonComponent, TextInputBuilder } from "discord.js";
+import Discord, { CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, ButtonComponent, TextInputBuilder, RoleSelectMenuBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../utils/client.js";
import { modalInteractionCollector } from "../../utils/dualCollector.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import keyValueList from "../../utils/generateKeyValueList.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
- .setName("commands")
+ .setName("moderation")
.setDescription("Links and text shown to a user after a moderator action is performed")
- .addRoleOption((o) => o.setName("role").setDescription("The role given when a member is muted"));
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- await interaction.reply({
+const callback = async (interaction: CommandInteraction): Promise<void> => {
+ const m = await interaction.reply({
embeds: LoadingEmbed,
ephemeral: true,
fetchReply: true
});
- let m;
- let clicked = "";
- if (interaction.options.get("role")) {
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Moderation Commands")
- .setDescription(
- keyValueList({
- role: `<@&${(interaction.options.get("role") as unknown as Role).id}>`
- })
- )
- .setColor("Danger")
- .send(true);
- if (confirmation.cancelled) return
- if (confirmation.success) {
- await client.database.guilds.write(interaction.guild!.id, {
- ["moderation.mute.role"]: (interaction.options.get("role") as unknown as Role).id
- });
- }
- }
let timedOut = false;
while (!timedOut) {
const config = await client.database.guilds.read(interaction.guild!.id);
const moderation = config.moderation;
- m = await interaction.editReply({
+ console.log(moderation)
+ await interaction.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Moderation Commands")
@@ -52,8 +30,7 @@
.setStatus("Success")
.setDescription(
"These links are shown below the message sent in a user's DM when they are punished.\n\n" +
- "**Mute Role:** " +
- (moderation.mute.role ? `<@&${moderation.mute.role}>` : "*None set*")
+ "**Mute Role:** " + (moderation.mute.role ? `<@&${moderation.mute.role}>` : "*None set*")
)
],
components: [
@@ -93,24 +70,23 @@
]),
new ActionRowBuilder<ButtonBuilder>().addComponents([
new ButtonBuilder()
- .setLabel(clicked === "clearMuteRole" ? "Click again to confirm" : "Clear mute role")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clearMuteRole")
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!moderation.mute.role),
- new ButtonBuilder()
.setCustomId("timeout")
.setLabel("Mute timeout " + (moderation.mute.timeout ? "Enabled" : "Disabled"))
.setStyle(moderation.mute.timeout ? ButtonStyle.Success : ButtonStyle.Danger)
.setEmoji(getEmojiByName("CONTROL." + (moderation.mute.timeout ? "TICK" : "CROSS"), "id"))
- ])
+ ]),
+ new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("muteRole")
+ .setPlaceholder("Select a new mute role")
+ )
]
});
let i;
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
@@ -118,20 +94,13 @@
}
type modIDs = "mute" | "kick" | "ban" | "softban" | "warn" | "role";
let chosen = moderation[i.customId as modIDs];
- if ((i.component as ButtonComponent).customId === "clearMuteRole") {
- i.deferUpdate();
- if (clicked === "clearMuteRole") {
- await client.database.guilds.write(interaction.guild!.id, {
- "moderation.mute.role": null
- });
- } else {
- clicked = "clearMuteRole";
- }
+ if (i.isRoleSelectMenu()) {
+ await i.deferUpdate();
+ await client.database.guilds.write(interaction.guild!.id, {
+ "moderation.mute.role": i.values[0]!
+ });
continue;
- } else {
- clicked = "";
- }
- if ((i.component as ButtonComponent).customId === "timeout") {
+ } else if ((i.component as ButtonComponent).customId === "timeout") {
await i.deferUpdate();
await client.database.guilds.write(interaction.guild!.id, {
"moderation.mute.timeout": !moderation.mute.timeout
@@ -183,15 +152,11 @@
});
let out: Discord.ModalSubmitInteraction | null;
try {
- out = await modalInteractionCollector(
- m,
- (m) => m.channel!.id === interaction.channel!.id,
- (_) => true
- ) as Discord.ModalSubmitInteraction | null;
+ out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
} catch (e) {
continue;
}
- if (!out) continue
+ if (!out || out.isButton()) continue
const buttonText = out.fields.getTextInputValue("name");
const buttonLink = out.fields.getTextInputValue("url").replace(/{id}/gi, "{id}");
const current = chosen;
@@ -206,9 +171,10 @@
}
}
}
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/rolemenu.ts b/src/commands/settings/rolemenu.ts
index b62d962..cccb6f6 100644
--- a/src/commands/settings/rolemenu.ts
+++ b/src/commands/settings/rolemenu.ts
@@ -1,19 +1,478 @@
import type Discord from "discord.js";
-import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, Message, ModalBuilder, RoleSelectMenuBuilder, RoleSelectMenuInteraction, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import client from "../../utils/client.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import createPageIndicator from "../../utils/createPageIndicator.js";
+import { configToDropdown } from "../../actions/roleMenu.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+import ellipsis from "../../utils/ellipsis.js";
+import lodash from 'lodash';
+
+const isEqual = lodash.isEqual;
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("rolemenu")
- .setDescription("rolemenu") // TODO
- .addRoleOption((option) => option.setName("role").setDescription("The role to give after verifying")); // FIXME FOR FUCK SAKE
+ .setDescription("rolemenu")
+
+interface ObjectSchema {
+ name: string;
+ description: string;
+ min: number;
+ max: number;
+ options: {
+ name: string;
+ description: string | null;
+ role: string;
+ }[];
+}
+
+const defaultRolePageConfig = {
+ name: "Role Menu Page",
+ description: "A new role menu page",
+ min: 0,
+ max: 0,
+ options: [
+ {name: "Role 1", description: null, role: "No role set"}
+ ]
+}
+
+const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, currentObj: ObjectSchema[]) => {
+ const reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("reorder")
+ .setPlaceholder("Select all pages in the order you want them to appear.")
+ .setMinValues(currentObj.length)
+ .setMaxValues(currentObj.length)
+ .addOptions(
+ currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
+ .setLabel(o.name)
+ .setValue(i.toString())
+ )
+ )
+ );
+ const buttonRow = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ )
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Role Menu")
+ .setDescription("Select pages in the order you want them to appear.")
+ .setStatus("Success")
+ ],
+ components: [reorderRow, buttonRow]
+ });
+ let out: StringSelectMenuInteraction | ButtonInteraction | null;
+ try {
+ out = await m.awaitMessageComponent({
+ filter: (i) => i.channel!.id === interaction.channel!.id,
+ time: 300000
+ }) as StringSelectMenuInteraction | ButtonInteraction | null;
+ } catch (e) {
+ console.error(e);
+ out = null;
+ }
+ if(!out) return;
+ out.deferUpdate();
+ if (out.isButton()) return;
+ const values = out.values;
+
+ const newOrder: ObjectSchema[] = currentObj.map((_, i) => {
+ const index = values.findIndex(v => v === i.toString());
+ return currentObj[index];
+ }) as ObjectSchema[];
+
+ return newOrder;
+}
+
+const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => {
+
+ let {name, description} = data;
+ const modal = new ModalBuilder()
+ .setTitle("Edit Name and Description")
+ .setCustomId("editNameDescription")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setLabel("Name")
+ .setCustomId("name")
+ .setPlaceholder("The name of the role (e.g. Programmer)")
+ .setStyle(TextInputStyle.Short)
+ .setValue(name ?? "")
+ .setRequired(true)
+ ),
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setLabel("Description")
+ .setCustomId("description")
+ .setPlaceholder("A short description of the role (e.g. A role for people who code)")
+ .setStyle(TextInputStyle.Short)
+ .setValue(description ?? "")
+ )
+ )
+ const button = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ )
+
+ await i.showModal(modal)
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Role Menu")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ ],
+ components: [button]
+ });
+
+ let out: Discord.ModalSubmitInteraction | null;
+ try {
+ out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
+ } catch (e) {
+ console.error(e);
+ out = null;
+ }
+ if(!out) return [name, description];
+ if (out.isButton()) return [name, description];
+ name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
+ description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description;
+ return [name, description]
+
+}
+
+const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => {
+ if (!data) data = {
+ name: "Role Menu Page",
+ description: "A new role menu page",
+ min: 0,
+ max: 0,
+ options: []
+ };
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("edit")
+ .setLabel("Edit")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("addRole")
+ .setLabel("Add Role")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
+ );
+
+ let back = false
+ if(data.options.length === 0) {
+ data.options = [
+ {name: "Role 1", description: null, role: "No role set"}
+ ]
+ }
+ do {
+ const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options});
+ const embed = new EmojiEmbed()
+ .setTitle(`${data.name}`)
+ .setStatus("Success")
+ .setDescription(
+ `**Description:**\n> ${data.description}\n\n` +
+ `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" +
+ `**Max:** ${data.max}\n`
+ )
+
+ interaction.editReply({embeds: [embed], components: [previewSelect, buttons]});
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ back = true;
+ break;
+ }
+
+ if (i.isStringSelectMenu()) {
+ if(i.customId === "roles") {
+ await i.deferUpdate();
+ await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0]));
+ }
+ } else if (i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ await i.deferUpdate();
+ back = true;
+ break;
+ }
+ case "edit": {
+ const [name, description] = await editNameDescription(i, interaction, m, data);
+ data.name = name ? name : data.name;
+ data.description = description ? description : data.description;
+ break;
+ }
+ case "addRole": {
+ await i.deferUpdate();
+ data.options.push(await createRoleMenuOptionPage(interaction, m));
+ break;
+ }
+ }
+ }
+
+ } while (!back);
+ if(isEqual(data, defaultRolePageConfig)) return null;
+ return data;
+}
+
+const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => {
+ const { renderRole} = client.logger;
+ if (!data) data = {
+ name: "New role Menu Option",
+ description: null,
+ role: ""
+ };
+ let back = false;
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("edit")
+ .setLabel("Edit Details")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji)
+ );
+ do {
+ const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role");
+ const embed = new EmojiEmbed()
+ .setTitle(`${data.name}`)
+ .setStatus("Success")
+ .setDescription(
+ `**Description:**\n> ${data.description ?? "No description set"}\n\n` +
+ `**Role:** ${data.role ? renderRole((await interaction.guild!.roles.fetch(data.role))!) : "No role set"}\n`
+ )
+
+ interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]});
+
+ let i: RoleSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | RoleSelectMenuInteraction;
+ } catch (e) {
+ back = true;
+ break;
+ }
+
+ if (i.isRoleSelectMenu()) {
+ if(i.customId === "role") {
+ await i.deferUpdate();
+ data.role = (i as RoleSelectMenuInteraction).values[0]!;
+ }
+ } else if (i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ await i.deferUpdate();
+ back = true;
+ break;
+ }
+ case "edit": {
+ await i.deferUpdate();
+ const [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string});
+ data.name = name ? name : data.name;
+ data.description = description ? description : data.description;
+ break;
+ }
+ }
+ }
+ } while (!back);
+ return data;
+}
const callback = async (interaction: CommandInteraction): Promise<void> => {
- console.log("we changed the charger again because fuck you");
- await interaction.reply("You're mum");
+ if (!interaction.guild) return;
+ const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
+
+ let page = 0;
+ let closed = false;
+ const config = await client.database.guilds.read(interaction.guild.id);
+ let currentObject: ObjectSchema[] = config.roleMenu.options;
+ let modified = false;
+ do {
+ const embed = new EmojiEmbed()
+ .setTitle("Role Menu")
+ .setEmoji("GUILD.GREEN")
+ .setStatus("Success");
+ const noRoleMenus = currentObject.length === 0;
+ let current: ObjectSchema;
+
+ const pageSelect = new StringSelectMenuBuilder()
+ .setCustomId("page")
+ .setPlaceholder("Select a Role Menu page to manage");
+ const actionSelect = new StringSelectMenuBuilder()
+ .setCustomId("action")
+ .setPlaceholder("Perform an action")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Edit")
+ .setDescription("Edit this page")
+ .setValue("edit")
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Delete")
+ .setDescription("Delete this page")
+ .setValue("delete")
+ .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
+ );
+ const buttonRow = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ .setDisabled(page === 0),
+ new ButtonBuilder()
+ .setCustomId("next")
+ .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Primary)
+ .setDisabled(page === Object.keys(currentObject).length - 1),
+ new ButtonBuilder()
+ .setCustomId("add")
+ .setLabel("New Page")
+ .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(Object.keys(currentObject).length >= 24),
+ new ButtonBuilder()
+ .setCustomId("reorder")
+ .setLabel("Reorder Pages")
+ .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(Object.keys(currentObject).length <= 1),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Success)
+ .setDisabled(!modified),
+ );
+ if(noRoleMenus) {
+ embed.setDescription("No role menu pages have been set up yet. Use the button below to add one.\n\n" +
+ createPageIndicator(1, 1, undefined, true)
+ );
+ pageSelect.setDisabled(true);
+ actionSelect.setDisabled(true);
+ pageSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel("No role menu pages")
+ .setValue("none")
+ );
+ } else {
+ page = Math.min(page, Object.keys(currentObject).length - 1);
+ current = currentObject[page]!;
+ embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
+ `**Description:**\n> ${current.description}\n` +
+ `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}`
+ );
+
+ pageSelect.addOptions(
+ currentObject.map((key: ObjectSchema, index) => {
+ return new StringSelectMenuOptionBuilder()
+ .setLabel(ellipsis(key.name, 50))
+ .setDescription(ellipsis(key.description, 50))
+ .setValue(index.toString());
+ })
+ );
+
+ }
+
+ await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+
+ await i.deferUpdate();
+ if (i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ page--;
+ break;
+ }
+ case "next": {
+ page++;
+ break;
+ }
+ case "add": {
+ const newPage = await editRoleMenuPage(i, m)
+ if(!newPage) break;
+ currentObject.push();
+ page = currentObject.length - 1;
+ break;
+ }
+ case "reorder": {
+ const reordered = await reorderRoleMenuPages(interaction, m, currentObject);
+ if(!reordered) break;
+ currentObject = reordered;
+ break;
+ }
+ case "save": {
+ client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject});
+ modified = false;
+ break;
+ }
+ }
+ } else if (i.isStringSelectMenu()) {
+ switch (i.customId) {
+ case "action": {
+ switch(i.values[0]) {
+ case "edit": {
+ const edited = await editRoleMenuPage(i, m, current!);
+ if(!edited) break;
+ currentObject[page] = edited;
+ modified = true;
+ break;
+ }
+ case "delete": {
+ if(page === 0 && currentObject.keys.length - 1 > 0) page++;
+ else page--;
+ currentObject.splice(page, 1);
+ break;
+ }
+ }
+ break;
+ }
+ case "page": {
+ page = parseInt(i.values[0]!);
+ break;
+ }
+ }
+ }
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageRoles"))
return "You must have the *Manage Roles* permission to use this command";
diff --git a/src/commands/settings/stats.ts b/src/commands/settings/stats.ts
index cdd218b..d46b57e 100644
--- a/src/commands/settings/stats.ts
+++ b/src/commands/settings/stats.ts
@@ -1,249 +1,403 @@
import { LoadingEmbed } from "../../utils/defaults.js";
-import Discord, { CommandInteraction, Message, ActionRowBuilder, GuildMember, StringSelectMenuBuilder, StringSelectMenuInteraction, AutocompleteInteraction } from "discord.js";
+import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, TextInputBuilder, StringSelectMenuInteraction, ButtonInteraction, MessageComponentInteraction, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, ModalBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../utils/client.js";
import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
-import { callback as statsChannelAddCallback } from "../../reflex/statsChannelUpdate.js";
import singleNotify from "../../utils/singleNotify.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import createPageIndicator from "../../utils/createPageIndicator.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("stats")
.setDescription("Controls channels which update when someone joins or leaves the server")
- .addChannelOption((option) => option.setName("channel").setDescription("The channel to modify"))
- .addStringOption((option) =>
- option
- .setName("name")
- .setDescription("The new channel name | Enter any text or use the extra variables like {memberCount}")
- .setAutocomplete(true)
- );
-const callback = async (interaction: CommandInteraction): Promise<unknown> => { // TODO: This command feels unintuitive. Clicking a channel in the select menu deletes it
- // instead, it should give a submenu to edit the channel, enable/disable or delete it
- singleNotify("statsChannelDeleted", interaction.guild!.id, true);
- const m = (await interaction.reply({
- embeds: LoadingEmbed,
- ephemeral: true,
- fetchReply: true
- })) as Message;
- let config = await client.database.guilds.read(interaction.guild!.id);
- if (interaction.options.get("name")?.value as string) {
- let channel;
- if (Object.keys(config.stats).length >= 25) {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Stats Channel")
- .setDescription("You can only have 25 stats channels in a server")
- .setStatus("Danger")
- ]
- });
- }
- try {
- channel = interaction.options.get("channel")?.channel as Discord.Channel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Stats 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("Stats Channel")
- .setDescription("You must choose a channel in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- let newName = await convertCurlyBracketString(
- interaction.options.get("name")?.value as string,
- "",
- "",
- interaction.guild!.name,
- interaction.guild!.members
- );
- if (interaction.options.get("channel")?.channel!.type === Discord.ChannelType.GuildText) {
- newName = newName.toLowerCase().replace(/[\s]/g, "-");
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("CHANNEL.TEXT.EDIT")
- .setTitle("Stats Channel")
- .setDescription(
- `Are you sure you want to set <#${channel.id}> to a stats channel?\n\n*Preview: ${newName.replace(
- /^ +| $/g,
- ""
- )}*`
- )
- .setColor("Warning")
- .setInverted(true)
- .setFailedMessage(`Could not convert <#${channel.id}> to a stats chanel.`, "Danger", "CHANNEL.TEXT.DELETE")
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- const name = interaction.options.get("name")?.value as string;
- const channel = interaction.options.get("channel")?.channel as Discord.TextChannel;
- await client.database.guilds.write(interaction.guild!.id, {
- [`stats.${channel.id}`]: { name: name, enabled: true }
- });
- const { log, NucleusColors, entry, renderUser, renderChannel } = client.logger;
- const data = {
- meta: {
- type: "statsChannelUpdate",
- displayName: "Stats 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)),
- name: entry(
- interaction.options.get("name")?.value as string,
- `\`${interaction.options.get("name")?.value as string}\``
+
+const showModal = async (interaction: MessageComponentInteraction, current: { enabled: boolean; name: string; }) => {
+ await interaction.showModal(
+ new ModalBuilder()
+ .setCustomId("modal")
+ .setTitle(`Stats channel name`)
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex1")
+ .setLabel("Server Info (1/3)")
+ .setPlaceholder(
+ `{serverName} - This server's name\n\n` +
+ `These placeholders will be replaced with the server's name, etc..`
)
- },
- hidden: {
- guild: interaction.guild!.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription("Something went wrong and the stats channel could not be set")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: []
- });
- }
- await statsChannelAddCallback(client, interaction.member as GuildMember);
- }
- let timedOut = false;
- while (!timedOut) {
- config = await client.database.guilds.read(interaction.guild!.id);
- const stats = config.stats;
- const selectMenu = new StringSelectMenuBuilder()
- .setCustomId("remove")
- .setMinValues(1)
- .setMaxValues(Math.max(1, Object.keys(stats).length));
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Stats Channel")
- .setDescription(
- "The following channels update when someone joins or leaves the server. You can select a channel to remove it from the list."
- )
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
- Object.keys(stats).length
- ? [
- selectMenu
- .setPlaceholder("Select a stats channel to remove, stopping it updating")
- .addOptions(
- Object.keys(stats).map((key) => ({
- label: interaction.guild!.channels.cache.get(key)!.name,
- value: key,
- description: `${stats[key]!.name}`
- }))
- )
- ]
- : [
- selectMenu
- .setPlaceholder("The server has no stats channels")
- .setDisabled(true)
- .setOptions([
- {
- label: "*Placeholder*",
- value: "placeholder",
- description: "No stats channels"
- }
- ])
- ]
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex2")
+ .setLabel("Member Counts (2/3) - {MemberCount:...}")
+ .setPlaceholder(
+ `{:all} - Total member count\n` +
+ `{:humans} - Total non-bot users\n` +
+ `{:bots} - Number of bots\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex3")
+ .setLabel("Latest Member (3/3) - {member:...}")
+ .setPlaceholder(
+ `{:name} - The members name\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(Discord.TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("text")
+ .setLabel("Channel name input")
+ .setMaxLength(1000)
+ .setRequired(true)
+ .setStyle(Discord.TextInputStyle.Short)
+ .setValue(current.name)
+ )
+ )
+ );
+}
+
+type ObjectSchema = Record<string, {name: string, enabled: boolean}>
+
+
+const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise<ObjectSchema> => {
+ let closed = false;
+ let cancelled = false;
+ const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}]));
+ let newChannel: string | undefined;
+ let newChannelName: string = "{memberCount:all}-members";
+ let newChannelEnabled: boolean = true;
+ do {
+ m = await interaction.editReply({
+ embeds: [new EmojiEmbed()
+ .setTitle("Stats Channel")
+ .setDescription(
+ `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" +
+ `**Name:** \`${newChannelName}\`\n` +
+ `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` +
+ `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n`
+ )
+ .setEmoji("SETTINGS.STATS.GREEN")
+ .setStatus("Success")
+ ], components: [
+ new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel to use")
+ ),
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setLabel("Cancel")
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId("back"),
+ new ButtonBuilder()
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
+ .setStyle(ButtonStyle.Success)
+ .setCustomId("save"),
+ new ButtonBuilder()
+ .setLabel("Edit name")
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("editName"),
+ new ButtonBuilder()
+ .setLabel(newChannelEnabled ? "Enabled" : "Disabled")
+ .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id"))
+ .setStyle(ButtonStyle.Secondary)
+ .setCustomId("toggleEnabled")
)
]
});
- let i: StringSelectMenuInteraction;
+ let i: ButtonInteraction | ChannelSelectMenuInteraction;
try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- }) as StringSelectMenuInteraction;
+ i = await m.awaitMessageComponent({ time: 300000, filter: (i) => {
+ return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id;
+ }}) as ButtonInteraction | ChannelSelectMenuInteraction;
} catch (e) {
- timedOut = true;
+ closed = true;
+ cancelled = true;
+ break;
+ }
+ if (i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ await i.deferUpdate();
+ closed = true;
+ break;
+ }
+ case "save": {
+ await i.deferUpdate();
+ if (newChannel) {
+ currentObject[newChannel] = {
+ name: newChannelName,
+ enabled: newChannelEnabled
+ }
+ }
+ closed = true;
+ break;
+ }
+ case "editName": {
+ await interaction.editReply({
+ embeds: [new EmojiEmbed()
+ .setTitle("Stats Channel")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("SETTINGS.STATS.GREEN")
+ ],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )
+ ]
+ });
+ showModal(i, {name: newChannelName, enabled: newChannelEnabled})
+
+ const out: Discord.ModalSubmitInteraction | ButtonInteraction| null = await modalInteractionCollector(m, interaction.user);
+ if (!out) continue;
+ if (out.isButton()) continue;
+ newChannelName = out.fields.getTextInputValue("text");
+ break;
+ }
+ case "toggleEnabled": {
+ await i.deferUpdate();
+ newChannelEnabled = !newChannelEnabled;
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ if (i.customId === "channel") {
+ newChannel = i.values[0];
+ }
+ }
+ } while (!closed)
+ if (cancelled) return originalObject;
+ if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject;
+ return currentObject;
+}
+const callback = async (interaction: CommandInteraction) => {
+ if (!interaction.guild) return;
+ const { renderChannel } = client.logger;
+ const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
+ let page = 0;
+ let closed = false;
+ const config = await client.database.guilds.read(interaction.guild.id);
+ let currentObject: ObjectSchema = config.stats;
+ let modified = false;
+ do {
+ const embed = new EmojiEmbed()
+ .setTitle("Stats Settings")
+ .setEmoji("SETTINGS.STATS.GREEN")
+ .setStatus("Success");
+ const noStatsChannels = Object.keys(currentObject).length === 0;
+ let current: { enabled: boolean; name: string; };
+
+ const pageSelect = new StringSelectMenuBuilder()
+ .setCustomId("page")
+ .setPlaceholder("Select a stats channel to manage");
+ const actionSelect = new StringSelectMenuBuilder()
+ .setCustomId("action")
+ .setPlaceholder("Perform an action")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Edit")
+ .setDescription("Edit the stats channel")
+ .setValue("edit")
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Delete")
+ .setDescription("Delete the stats channel")
+ .setValue("delete")
+ .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
+ );
+ const buttonRow = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ .setDisabled(page === 0),
+ new ButtonBuilder()
+ .setCustomId("next")
+ .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Primary)
+ .setDisabled(page === Object.keys(currentObject).length - 1),
+ new ButtonBuilder()
+ .setCustomId("add")
+ .setLabel("Create new")
+ .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(Object.keys(currentObject).length >= 24),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Success)
+ .setDisabled(modified),
+ );
+ if (noStatsChannels) {
+ embed.setDescription("No stats channels have been set up yet. Use the button below to add one.\n\n" +
+ createPageIndicator(1, 1, undefined, true)
+ );
+ pageSelect.setDisabled(true);
+ actionSelect.setDisabled(true);
+ pageSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel("No stats channels")
+ .setValue("none")
+ );
+ } else {
+ page = Math.min(page, Object.keys(currentObject).length - 1);
+ current = currentObject[Object.keys(config.stats)[page]!]!
+ actionSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel(current.enabled ? "Disable" : "Enable")
+ .setValue("toggleEnabled")
+ .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
+ .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
+ );
+ embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` +
+ `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
+ `**Name:** \`${current.name}\`\n` +
+ `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' +
+ createPageIndicator(Object.keys(config.stats).length, page)
+ );
+ for (const [id, { name, enabled }] of Object.entries(currentObject)) {
+ pageSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel(`${name} (${renderChannel(id)})`)
+ .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
+ .setDescription(`${enabled ? "Enabled" : "Disabled"}`)
+ .setValue(id)
+ );
+ }
+ }
+
+ interaction.editReply({embeds: [embed], components: [
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
+ buttonRow
+ ]});
+
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction;
+ } catch (e) {
+ closed = true;
continue;
}
- i.deferUpdate();
- if (i.customId === "remove") {
- const toRemove = i.values;
- await client.database.guilds.write(
- interaction.guild!.id,
- null,
- toRemove.map((k) => `stats.${k}`)
- );
+
+ if(i.isStringSelectMenu()) {
+ switch(i.customId) {
+ case "page": {
+ await i.deferUpdate();
+ page = Object.keys(currentObject).indexOf(i.values[0]!);
+ break;
+ }
+ case "action": {
+ modified = true;
+ switch(i.values[0]!) {
+ case "edit": {
+ showModal(i, current!)
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Stats Channel")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ .setEmoji("SETTINGS.STATS.GREEN")
+ ],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
+ new ButtonBuilder()
+ .setLabel("Back")
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("back")
+ )
+ ]
+ });
+ let out: Discord.ModalSubmitInteraction | null;
+ try {
+ out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
+ } catch (e) {
+ continue;
+ }
+ if (!out) continue
+ if (out.isButton()) continue;
+ currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text");
+ break;
+ }
+ case "toggleEnabled": {
+ await i.deferUpdate();
+ currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled;
+ modified = true;
+ break;
+ }
+ case "delete": {
+ await i.deferUpdate();
+ currentObject = Object.fromEntries(Object.entries(currentObject).filter(([k]) => k !== Object.keys(currentObject)[page]!));
+ page = Math.min(page, Object.keys(currentObject).length - 1);
+ modified = true;
+ break;
+ }
+ }
+ break;
+ }
+ }
+ } else {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "back": {
+ page--;
+ break;
+ }
+ case "next": {
+ page++;
+ break;
+ }
+ case "add": {
+ currentObject = await addStatsChannel(interaction, m, currentObject);
+ page = Object.keys(currentObject).length - 1;
+ break;
+ }
+ case "save": {
+ client.database.guilds.write(interaction.guild.id, {stats: currentObject});
+ singleNotify("statsChannelDeleted", interaction.guild.id, true);
+ modified = false;
+ break;
+ }
+ }
}
- }
- await interaction.editReply({
- embeds: [new Discord.EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })],
- components: []
- });
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageChannels"))
return "You must have the *Manage Channels* permission to use this command";
return true;
};
-const generateStatsChannelAutocomplete = (prompt: string): string[] => {
- return [prompt];
-};
-
-const autocomplete = async (interaction: AutocompleteInteraction): Promise<string[]> => {
- if (!interaction.guild) return [];
- const prompt = interaction.options.getString("tag");
- // generateStatsChannelAutocomplete(int.options.getString("name") ?? "")
- const results = generateStatsChannelAutocomplete(prompt ?? "");
- return results;
-};
-
-
export { command };
export { callback };
-export { check };
-export { autocomplete };
\ No newline at end of file
+export { check };
\ No newline at end of file
diff --git a/src/commands/settings/tickets.ts b/src/commands/settings/tickets.ts
index 892a420..2e046bf 100644
--- a/src/commands/settings/tickets.ts
+++ b/src/commands/settings/tickets.ts
@@ -1,68 +1,38 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
import Discord, {
CommandInteraction,
- GuildChannel,
Message,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
StringSelectMenuBuilder,
- Role,
- StringSelectMenuInteraction,
ButtonStyle,
TextInputBuilder,
ButtonComponent,
- StringSelectMenuComponent,
ModalSubmitInteraction,
- APIMessageComponentEmoji
+ APIMessageComponentEmoji,
+ RoleSelectMenuBuilder,
+ ChannelSelectMenuBuilder,
+ RoleSelectMenuInteraction,
+ ButtonInteraction,
+ ChannelSelectMenuInteraction,
+ TextInputStyle,
+ ModalBuilder,
+ ChannelType
} from "discord.js";
-import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "@discordjs/builders";
-import { ChannelType } from "discord-api-types/v9";
+import { SlashCommandSubcommandBuilder, StringSelectMenuOptionBuilder } from "discord.js";
import client from "../../utils/client.js";
import { toHexInteger, toHexArray, tickets as ticketTypes } from "../../utils/calculate.js";
import { capitalize } from "../../utils/generateKeyValueList.js";
import { modalInteractionCollector } from "../../utils/dualCollector.js";
import type { GuildConfig } from "../../utils/database.js";
+import { LinkWarningFooter } from "../../utils/defaults.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("tickets")
- .setDescription("Shows settings for tickets | Use no arguments to manage custom types")
- .addStringOption((option) =>
- option
- .setName("enabled")
- .setDescription("If users should be able to create tickets")
- .setRequired(false)
- .addChoices(
- {name: "Yes", value: "yes"},
- {name: "No",value: "no"}
- )
- )
- .addChannelOption((option) =>
- option
- .setName("category")
- .setDescription("The category where tickets are created")
- .addChannelTypes(ChannelType.GuildCategory)
- .setRequired(false)
- )
- .addNumberOption((option) =>
- option
- .setName("maxticketsperuser")
- .setDescription("The maximum amount of tickets a user can create | Default: 5")
- .setRequired(false)
- .setMinValue(1)
- )
- .addRoleOption((option) =>
- option
- .setName("supportrole")
- .setDescription(
- "This role will have view access to all tickets and will be pinged when a ticket is created"
- )
- .setRequired(false)
- );
+ .setDescription("Shows settings for tickets")
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
if (!interaction.guild) return;
@@ -71,392 +41,130 @@
ephemeral: true,
fetchReply: true
})) as Message;
- const options = {
- enabled: (interaction.options.get("enabled")?.value as string).startsWith("yes") as boolean | null,
- category: interaction.options.get("category")?.channel as Discord.CategoryChannel | null,
- maxtickets: interaction.options.get("maxticketsperuser")?.value as number | null,
- supportping: interaction.options.get("supportrole")?.role as Role | null
- };
- if (options.enabled !== null || options.category || options.maxtickets || options.supportping) {
- if (options.category) {
- let channel: GuildChannel | null;
- try {
- channel = await interaction.guild.channels.fetch(options.category.id) as GuildChannel;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("CHANNEL.TEXT.DELETE")
- .setTitle("Tickets > Category")
- .setDescription("The channel you provided is not a valid category")
- .setStatus("Danger")
- ]
- });
- }
- channel = channel as Discord.CategoryChannel;
- if (channel.guild.id !== interaction.guild.id)
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets > Category")
- .setDescription("You must choose a category in this server")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- if (options.maxtickets) {
- if (options.maxtickets < 1)
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets > Max Tickets")
- .setDescription("You must choose a number greater than 0")
- .setStatus("Danger")
- .setEmoji("CHANNEL.TEXT.DELETE")
- ]
- });
- }
- let role: Role | null;
- if (options.supportping) {
- try {
- role = await interaction.guild.roles.fetch(options.supportping.id);
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLE.DELETE")
- .setTitle("Tickets > Support Ping")
- .setDescription("The role you provided is not a valid role")
- .setStatus("Danger")
- ]
- });
- }
- if (!role) return;
- role = role as Discord.Role;
- if (role.guild.id !== interaction.guild.id)
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets > Support Ping")
- .setDescription("You must choose a role in this server")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLE.DELETE")
- ]
- });
- }
-
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.TICKET.ARCHIVED")
- .setTitle("Tickets")
- .setDescription(
- (options.category ? `**Category:** ${options.category.name}\n` : "") +
- (options.maxtickets ? `**Max Tickets:** ${options.maxtickets}\n` : "") +
- (options.supportping ? `**Support Ping:** ${options.supportping.name}\n` : "") +
- (options.enabled !== null
- ? `**Enabled:** ${
- options.enabled
- ? `${getEmojiByName("CONTROL.TICK")} Yes`
- : `${getEmojiByName("CONTROL.CROSS")} No`
- }\n`
- : "") +
- "\nAre you sure you want to apply these settings?"
- )
- .setColor("Warning")
- .setFailedMessage("Cancelled", "Warning", "GUILD.TICKET.CLOSE") // TODO: Set Actual Message
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- const toUpdate: Record<string, string | boolean | number> = {};
- if (options.enabled !== null) toUpdate["tickets.enabled"] = options.enabled;
- if (options.category) toUpdate["tickets.category"] = options.category.id;
- if (options.maxtickets) toUpdate["tickets.maxTickets"] = options.maxtickets;
- if (options.supportping) toUpdate["tickets.supportRole"] = options.supportping.id;
- try {
- await client.database.guilds.write(interaction.guild.id, toUpdate);
- } catch (e) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets")
- .setDescription("Something went wrong and the staff notifications channel could not be set")
- .setStatus("Danger")
- .setEmoji("GUILD.TICKET.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Tickets")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: []
- });
- }
- }
const data = await client.database.guilds.read(interaction.guild.id);
data.tickets.customTypes = (data.tickets.customTypes ?? []).filter(
(value: string, index: number, array: string[]) => array.indexOf(value) === index
);
- let lastClicked = "";
- const embed: EmojiEmbed = new EmojiEmbed();
- const compiledData = {
- enabled: data.tickets.enabled,
- category: data.tickets.category,
- maxTickets: data.tickets.maxTickets,
- supportRole: data.tickets.supportRole,
- useCustom: data.tickets.useCustom,
- types: data.tickets.types,
- customTypes: data.tickets.customTypes as string[] | null
- };
+ let ticketData = (await client.database.guilds.read(interaction.guild.id)).tickets
+ let changesMade = false;
let timedOut = false;
+ let errorMessage = "";
while (!timedOut) {
- embed
+ const embed: EmojiEmbed = new EmojiEmbed()
.setTitle("Tickets")
.setDescription(
- `${compiledData.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${
- compiledData.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`
+ `${ticketData.enabled ? "" : getEmojiByName("TICKETS.REPORT")} **Enabled:** ${
+ ticketData.enabled ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`
}\n` +
- `${compiledData.category ? "" : getEmojiByName("TICKETS.REPORT")} **Category:** ${
- compiledData.category ? `<#${compiledData.category}>` : "*None set*"
- }\n` +
- `**Max Tickets:** ${compiledData.maxTickets ? compiledData.maxTickets : "*No limit*"}\n` +
- `**Support Ping:** ${compiledData.supportRole ? `<@&${compiledData.supportRole}>` : "*None set*"}\n\n` +
- (compiledData.useCustom && compiledData.customTypes === null ? `${getEmojiByName("TICKETS.REPORT")} ` : "") +
- `${compiledData.useCustom ? "Custom" : "Default"} types in use` +
+ `${ticketData.category ? "" : getEmojiByName("TICKETS.REPORT")}` +
+ ((await interaction.guild.channels.fetch(ticketData.category!))!.type === ChannelType.GuildCategory ?
+ `**Category:** ` : `**Channel:** `) + // TODO: Notify if permissions are wrong
+ `${ticketData.category ? `<#${ticketData.category}>` : "*None set*"}\n` +
+ `**Max Tickets:** ${ticketData.maxTickets ? ticketData.maxTickets : "*No limit*"}\n` +
+ `**Support Ping:** ${ticketData.supportRole ? `<@&${ticketData.supportRole}>` : "*None set*"}\n\n` +
+ (ticketData.useCustom && ticketData.customTypes === null ? `${getEmojiByName("TICKETS.REPORT")} ` : "") +
+ `${ticketData.useCustom ? "Custom" : "Default"} types in use` +
"\n\n" +
`${getEmojiByName("TICKETS.REPORT")} *Indicates a setting stopping tickets from being used*`
)
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN");
+ if (errorMessage) embed.setFooter({text: errorMessage, iconURL: LinkWarningFooter.iconURL});
m = (await interaction.editReply({
embeds: [embed],
components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
+ new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
- .setLabel("Tickets " + (compiledData.enabled ? "enabled" : "disabled"))
- .setEmoji(getEmojiByName("CONTROL." + (compiledData.enabled ? "TICK" : "CROSS"), "id"))
- .setStyle(compiledData.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setLabel("Tickets " + (ticketData.enabled ? "enabled" : "disabled"))
+ .setEmoji(getEmojiByName("CONTROL." + (ticketData.enabled ? "TICK" : "CROSS"), "id"))
+ .setStyle(ticketData.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
.setCustomId("enabled"),
new ButtonBuilder()
- .setLabel(lastClicked === "cat" ? "Click again to confirm" : "Clear category")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setCustomId("clearCategory")
- .setDisabled(compiledData.category === null),
- new ButtonBuilder()
- .setLabel(lastClicked === "max" ? "Click again to confirm" : "Reset max tickets")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setCustomId("clearMaxTickets")
- .setDisabled(compiledData.maxTickets === 5),
- new ButtonBuilder()
- .setLabel(lastClicked === "sup" ? "Click again to confirm" : "Clear support ping")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setCustomId("clearSupportPing")
- .setDisabled(compiledData.supportRole === null)
- ]),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
+ .setLabel("Set max tickets")
+ .setEmoji(getEmojiByName("CONTROL.TICKET", "id"))
+ .setStyle(ButtonStyle.Primary)
+ .setCustomId("setMaxTickets")
+ .setDisabled(!ticketData.enabled),
new ButtonBuilder()
.setLabel("Manage types")
.setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
.setStyle(ButtonStyle.Secondary)
- .setCustomId("manageTypes"),
+ .setCustomId("manageTypes")
+ .setDisabled(!ticketData.enabled),
new ButtonBuilder()
- .setLabel("Add create ticket button")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Primary)
- .setCustomId("send")
- ])
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
+ .setStyle(ButtonStyle.Success)
+ .setCustomId("save")
+ .setDisabled(!changesMade)
+ ),
+ new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("supportRole")
+ .setPlaceholder("Select a support role")
+ .setDisabled(!ticketData.enabled)
+ ),
+ new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("category")
+ .setPlaceholder("Select a category or channel")
+ .setDisabled(!ticketData.enabled)
+ )
]
- })) as Message;
- let i: MessageComponentInteraction;
+ }));
+ let i: RoleSelectMenuInteraction | ButtonInteraction | ChannelSelectMenuInteraction;
try {
- i = await m.awaitMessageComponent({
+ i = await m.awaitMessageComponent<2 | 6 | 8>({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
continue;
}
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clearCategory") {
- if (lastClicked === "cat") {
- lastClicked = "";
- await client.database.guilds.write(interaction.guild.id, null, ["tickets.category"]);
- compiledData.category = null;
- } else lastClicked = "cat";
- } else if ((i.component as ButtonComponent).customId === "clearMaxTickets") {
- if (lastClicked === "max") {
- lastClicked = "";
- await client.database.guilds.write(interaction.guild.id, null, ["tickets.maxTickets"]);
- compiledData.maxTickets = 5;
- } else lastClicked = "max";
- } else if ((i.component as ButtonComponent).customId === "clearSupportPing") {
- if (lastClicked === "sup") {
- lastClicked = "";
- await client.database.guilds.write(interaction.guild.id, null, ["tickets.supportRole"]);
- compiledData.supportRole = null;
- } else lastClicked = "sup";
- } else if ((i.component as ButtonComponent).customId === "send") {
- const ticketMessages = [
- {
- label: "Create ticket",
- description: "Click the button below to create a ticket"
- },
- {
- label: "Issues, questions or feedback?",
- description: "Click below to open a ticket and get help from our staff team"
- },
- {
- label: "Contact Us",
- description: "Click the button below to speak to us privately"
+ changesMade = true;
+ if (i.isRoleSelectMenu()) {
+ await i.deferUpdate();
+ ticketData.supportRole = i.values[0] ?? null;
+ } else if (i.isChannelSelectMenu()) {
+ await i.deferUpdate();
+ ticketData.category = i.values[0] ?? null;
+ } else {
+ switch(i.customId) {
+ case "save": {
+ await i.deferUpdate();
+ await client.database.guilds.write(interaction.guild.id, { tickets: ticketData });
+ changesMade = false;
+ break;
}
- ];
- let innerTimedOut = false;
- let templateSelected = false;
- while (!innerTimedOut && !templateSelected) {
- const enabled = compiledData.enabled && compiledData.category !== null;
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Ticket Button")
- .setDescription("Select a message template to send in this channel")
- .setFooter({
- text: enabled
- ? ""
- : "Tickets are not set up correctly so the button may not work for users. Check the main menu to find which options must be set."
- })
- .setStatus(enabled ? "Success" : "Warning")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([
- new StringSelectMenuBuilder()
- .setOptions(
- ticketMessages.map(
- (
- t: {
- label: string;
- description: string;
- value?: string;
- },
- index
- ) => {
- t.value = index.toString();
- return t as {
- value: string;
- label: string;
- description: string;
- };
- }
- )
- )
- .setCustomId("template")
- .setMaxValues(1)
- .setMinValues(1)
- .setPlaceholder("Select a message template")
- ]),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("back")
- .setLabel("Back")
- .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder().setCustomId("blank").setLabel("Empty").setStyle(ButtonStyle.Secondary),
- new ButtonBuilder()
- .setCustomId("custom")
- .setLabel("Custom")
- .setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
- .setStyle(ButtonStyle.Primary)
- ])
- ]
- });
- let i: MessageComponentInteraction;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- innerTimedOut = true;
- continue;
+ case "enabled": {
+ await i.deferUpdate();
+ ticketData.enabled = !ticketData.enabled;
+ break;
}
- if ((i.component as StringSelectMenuComponent).customId === "template") {
- i.deferUpdate();
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(ticketMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.label)
- .setDescription(
- ticketMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.description
- )
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Create Ticket")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("createticket")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "blank") {
- i.deferUpdate();
- await interaction.channel!.send({
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Create Ticket")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("createticket")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "custom") {
+ case "setMaxTickets": {
await i.showModal(
- new Discord.ModalBuilder()
- .setCustomId("modal")
- .setTitle("Enter embed details")
+ new ModalBuilder()
+ .setCustomId("maxTickets")
+ .setTitle("Set max tickets")
.addComponents(
- new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new ActionRowBuilder<TextInputBuilder>().setComponents(
new TextInputBuilder()
- .setCustomId("title")
- .setLabel("Title")
- .setMaxLength(256)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Short)
- ),
- new ActionRowBuilder<TextInputBuilder>().addComponents(
- new TextInputBuilder()
- .setCustomId("description")
- .setLabel("Description")
- .setMaxLength(4000)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Paragraph)
+ .setLabel("Max tickets - Leave blank for no limit")
+ .setCustomId("maxTickets")
+ .setPlaceholder("Enter a number")
+ .setRequired(false)
+ .setValue(ticketData.maxTickets.toString())
+ .setMinLength(1)
+ .setMaxLength(3)
+ .setStyle(TextInputStyle.Short)
)
)
- );
- await interaction.editReply({
+ )
+ await i.editReply({
embeds: [
new EmojiEmbed()
- .setTitle("Ticket Button")
+ .setTitle("Tickets")
.setDescription("Modal opened. If you can't see it, click back and try again.")
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
@@ -473,54 +181,33 @@
});
let out;
try {
- out = await modalInteractionCollector(
- m,
- (m) => m.channel!.id === interaction.channel!.id,
- (m) => m.customId === "modify"
- );
+ out = await modalInteractionCollector(m, interaction.user);
} catch (e) {
- innerTimedOut = true;
continue;
}
+ if (!out || out.isButton()) continue;
out = out as ModalSubmitInteraction;
- const title = out.fields.getTextInputValue("title");
- const description = out.fields.getTextInputValue("description");
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(title)
- .setDescription(description)
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Create Ticket")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("createticket")
- ])
- ]
- });
- templateSelected = true;
+ const toAdd = out.fields.getTextInputValue("maxTickets");
+ if(isNaN(parseInt(toAdd))) {
+ errorMessage = "You entered an invalid number - No changes were made";
+ break;
+ }
+ ticketData.maxTickets = toAdd === "" ? 0 : parseInt(toAdd);
+ break;
+ }
+ case "manageTypes": {
+ await i.deferUpdate();
+ ticketData = await manageTypes(interaction, data.tickets, m);
+ break;
}
}
- } else if ((i.component as ButtonComponent).customId === "enabled") {
- await client.database.guilds.write(interaction.guild.id, {
- "tickets.enabled": !compiledData.enabled
- });
- compiledData.enabled = !compiledData.enabled;
- } else if ((i.component as ButtonComponent).customId === "manageTypes") {
- data.tickets = await manageTypes(interaction, data.tickets, m as Message);
}
}
- await interaction.editReply({
- embeds: [ embed.setFooter({ text: "Message timed out" })],
- components: []
- });
+ await interaction.deleteReply()
};
+
+
async function manageTypes(interaction: CommandInteraction, data: GuildConfig["tickets"], m: Message) {
let timedOut = false;
let backPressed = false;
@@ -545,7 +232,7 @@
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
],
- components: (customTypes
+ components: (customTypes && customTypes.length > 0
? [
new ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>().addComponents([
new Discord.StringSelectMenuBuilder()
@@ -637,29 +324,23 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
continue;
}
- if ((i.component as StringSelectMenuComponent).customId === "types") {
- i.deferUpdate();
- const types = toHexInteger((i as StringSelectMenuInteraction).values, ticketTypes);
- await client.database.guilds.write(interaction.guild!.id, {
- "tickets.types": types
- });
+ if (i.isStringSelectMenu() && i.customId === "types") {
+ await i.deferUpdate();
+ const types = toHexInteger(i.values, ticketTypes);
data.types = types;
- } else if ((i.component as StringSelectMenuComponent).customId === "removeTypes") {
- i.deferUpdate();
- const types = (i as StringSelectMenuInteraction).values;
+ } else if (i.isStringSelectMenu() && i.customId === "removeTypes") {
+ await i.deferUpdate();
+ const types = i.values;
let customTypes = data.customTypes;
if (customTypes) {
customTypes = customTypes.filter((t) => !types.includes(t));
customTypes = customTypes.length > 0 ? customTypes : null;
- await client.database.guilds.write(interaction.guild!.id, {
- "tickets.customTypes": customTypes
- });
data.customTypes = customTypes;
}
} else if ((i.component as ButtonComponent).customId === "addType") {
@@ -680,7 +361,7 @@
)
)
);
- await interaction.editReply({
+ await i.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Tickets > Types")
@@ -700,14 +381,11 @@
});
let out;
try {
- out = await modalInteractionCollector(
- m,
- (m) => m.channel!.id === interaction.channel!.id,
- (m) => m.customId === "addType"
- );
+ out = await modalInteractionCollector(m, interaction.user);
} catch (e) {
continue;
}
+ if (!out || out.isButton()) continue;
out = out as ModalSubmitInteraction;
let toAdd = out.fields.getTextInputValue("type");
if (!toAdd) {
@@ -715,31 +393,31 @@
}
toAdd = toAdd.substring(0, 80);
try {
- await client.database.guilds.append(interaction.guild!.id, "tickets.customTypes", toAdd);
+ if(!data.customTypes) data.customTypes = [];
+ data.customTypes.push(toAdd);
} catch {
continue;
}
- data.customTypes = data.customTypes ?? [];
if (!data.customTypes.includes(toAdd)) {
data.customTypes.push(toAdd);
}
} else if ((i.component as ButtonComponent).customId === "switchToDefault") {
- i.deferUpdate();
+ await i.deferUpdate();
await client.database.guilds.write(interaction.guild!.id, { "tickets.useCustom": false }, []);
data.useCustom = false;
} else if ((i.component as ButtonComponent).customId === "switchToCustom") {
- i.deferUpdate();
+ await i.deferUpdate();
await client.database.guilds.write(interaction.guild!.id, { "tickets.useCustom": true }, []);
data.useCustom = true;
} else {
- i.deferUpdate();
+ await i.deferUpdate();
backPressed = true;
}
}
return data;
}
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/tracks.ts b/src/commands/settings/tracks.ts
new file mode 100644
index 0000000..d9d485d
--- /dev/null
+++ b/src/commands/settings/tracks.ts
@@ -0,0 +1,459 @@
+import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, CommandInteraction, GuildMember, Message, ModalBuilder, ModalSubmitInteraction, PermissionsBitField, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
+import client from "../../utils/client.js";
+import createPageIndicator, { createVerticalTrack } from "../../utils/createPageIndicator.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import ellipsis from "../../utils/ellipsis.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
+
+const { renderRole } = client.logger
+
+const command = (builder: SlashCommandSubcommandBuilder) =>
+ builder
+ .setName("tracks")
+ .setDescription("Manage the tracks for the server")
+
+interface ObjectSchema {
+ name: string;
+ retainPrevious: boolean;
+ nullable: boolean;
+ track: string[];
+ manageableBy: string[];
+}
+
+
+const editName = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, current?: string) => {
+
+ let name = current ?? "";
+ const modal = new ModalBuilder()
+ .setTitle("Edit Name and Description")
+ .setCustomId("editNameDescription")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setLabel("Name")
+ .setCustomId("name")
+ .setPlaceholder("The name of the track (e.g. Moderators)")
+ .setStyle(TextInputStyle.Short)
+ .setValue(name)
+ .setRequired(true)
+ )
+ )
+ const button = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ )
+
+ await i.showModal(modal)
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Tracks")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ ],
+ components: [button]
+ });
+
+ let out: ModalSubmitInteraction | null;
+ try {
+ out = await modalInteractionCollector(m, interaction.user) as ModalSubmitInteraction | null;
+ } catch (e) {
+ console.error(e);
+ out = null;
+ }
+ if(!out) return name;
+ if (out.isButton()) return name;
+ name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
+ return name
+
+}
+
+const reorderTracks = async (interaction: ButtonInteraction, m: Message, roles: Collection<string, Role>, currentObj: string[]) => {
+ const reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("reorder")
+ .setPlaceholder("Select all roles in the order you want users to gain them (Lowest to highest rank).")
+ .setMinValues(currentObj.length)
+ .setMaxValues(currentObj.length)
+ .addOptions(
+ currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
+ .setLabel(roles.get(o)!.name)
+ .setValue(i.toString())
+ )
+ )
+ );
+ const buttonRow = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ )
+ await interaction.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Tracks")
+ .setDescription("Select all roles in the order you want users to gain them (Lowest to highest rank).")
+ .setStatus("Success")
+ ],
+ components: [reorderRow, buttonRow]
+ });
+ let out: StringSelectMenuInteraction | ButtonInteraction | null;
+ try {
+ out = await m.awaitMessageComponent({
+ filter: (i) => i.channel!.id === interaction.channel!.id,
+ time: 300000
+ }) as StringSelectMenuInteraction | ButtonInteraction | null;
+ } catch (e) {
+ console.error(e);
+ out = null;
+ }
+ if(!out) return;
+ out.deferUpdate();
+ if (out.isButton()) return;
+ const values = out.values;
+
+ const newOrder: string[] = currentObj.map((_, i) => {
+ const index = values.findIndex(v => v === i.toString());
+ return currentObj[index];
+ }) as string[];
+
+ return newOrder;
+}
+
+const editTrack = async (interaction: ButtonInteraction | StringSelectMenuInteraction, message: Message, roles: Collection<string, Role>, current?: ObjectSchema) => {
+ const isAdmin = (interaction.member!.permissions as PermissionsBitField).has("Administrator");
+ if(!current) {
+ current = {
+ name: "",
+ retainPrevious: false,
+ nullable: false,
+ track: [],
+ manageableBy: []
+ }
+ }
+
+ const roleSelect = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("addRole")
+ .setPlaceholder("Select a role to add")
+ .setDisabled(!isAdmin)
+ );
+ let closed = false;
+ do {
+ const editableRoles: string[] = current.track.map((r) => {
+ if(!(roles.get(r)!.position >= (interaction.member as GuildMember).roles.highest.position) || interaction.user.id === interaction.guild?.ownerId) return roles.get(r)!.name;
+ }).filter(v => v !== undefined) as string[];
+ const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
+ .addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("removeRole")
+ .setPlaceholder("Select a role to remove")
+ .setDisabled(!isAdmin)
+ .addOptions(
+ editableRoles.map((r, i) => {
+ return new StringSelectMenuOptionBuilder()
+ .setLabel(r)
+ .setValue(i.toString())}
+ )
+ )
+ );
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("edit")
+ .setLabel("Edit Name")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("reorder")
+ .setLabel("Reorder")
+ .setDisabled(!isAdmin)
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("retainPrevious")
+ .setLabel("Retain Previous")
+ .setStyle(current.retainPrevious ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("nullable")
+ .setLabel(`Role ${current.nullable ? "Not " : ""}Required`)
+ .setStyle(current.nullable ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL." + (current.nullable ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji)
+ );
+
+ const allowed: boolean[] = [];
+ for (const role of current.track) {
+ const disabled: boolean =
+ roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position;
+ allowed.push(disabled)
+ }
+ const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!);
+
+ const embed = new EmojiEmbed()
+ .setTitle("Tracks")
+ .setDescription(
+ `**Currently Editing:** ${current.name}\n\n` +
+ `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` +
+ `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` +
+ createVerticalTrack(
+ mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false), allowed)
+ )
+ .setStatus("Success")
+
+ const comps: ActionRowBuilder<RoleSelectMenuBuilder | ButtonBuilder | StringSelectMenuBuilder>[] = [roleSelect, buttons];
+ if(current.track.length >= 1) comps.splice(1, 0, selectMenu);
+
+ interaction.editReply({embeds: [embed], components: comps});
+
+ let out: ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null;
+
+ try {
+ out = await message.awaitMessageComponent({
+ filter: (i) => i.channel!.id === interaction.channel!.id,
+ time: 300000
+ }) as ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null;
+ } catch (e) {
+ console.error(e);
+ out = null;
+ }
+
+ if(!out) return;
+ if (out.isButton()) {
+ switch(out.customId) {
+ case "back": {
+ out.deferUpdate();
+ closed = true;
+ break;
+ }
+ case "edit": {
+ current.name = (await editName(out, interaction, message, current.name))!;
+ break;
+ }
+ case "reorder": {
+ out.deferUpdate();
+ current.track = (await reorderTracks(out, message, roles, current.track))!;
+ break;
+ }
+ case "retainPrevious": {
+ out.deferUpdate();
+ current.retainPrevious = !current.retainPrevious;
+ break;
+ }
+ case "nullable": {
+ out.deferUpdate();
+ current.nullable = !current.nullable;
+ break;
+ }
+ }
+ } else if (out.isStringSelectMenu()) {
+ out.deferUpdate();
+ switch(out.customId) {
+ case "removeRole": {
+ const index = current.track.findIndex(v => v === editableRoles[parseInt((out! as StringSelectMenuInteraction).values![0]!)]);
+ current.track.splice(index, 1);
+ break;
+ }
+ }
+ } else {
+ switch(out.customId) {
+ case "addRole": {
+ const role = out.values![0]!;
+ if(!current.track.includes(role)) {
+ current.track.push(role);
+ } else {
+ out.reply({content: "That role is already on this track", ephemeral: true})
+ }
+ break;
+ }
+ }
+ }
+
+ } while(!closed);
+ return current;
+}
+
+const callback = async (interaction: CommandInteraction) => {
+
+ const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true})
+ const config = await client.database.guilds.read(interaction.guild!.id);
+ const tracks: ObjectSchema[] = config.tracks;
+ const roles = await interaction.guild!.roles.fetch();
+
+ let page = 0;
+ let closed = false;
+ let modified = false;
+
+ do {
+ const embed = new EmojiEmbed()
+ .setTitle("Track Settings")
+ .setEmoji("TRACKS.ICON")
+ .setStatus("Success");
+ const noTracks = config.tracks.length === 0;
+ let current: ObjectSchema;
+
+ const pageSelect = new StringSelectMenuBuilder()
+ .setCustomId("page")
+ .setPlaceholder("Select a track to manage");
+ const actionSelect = new StringSelectMenuBuilder()
+ .setCustomId("action")
+ .setPlaceholder("Perform an action")
+ .addOptions(
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Edit")
+ .setDescription("Edit this track")
+ .setValue("edit")
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new StringSelectMenuOptionBuilder()
+ .setLabel("Delete")
+ .setDescription("Delete this track")
+ .setValue("delete")
+ .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
+ );
+ const buttonRow = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ .setDisabled(page === 0),
+ new ButtonBuilder()
+ .setCustomId("next")
+ .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Primary)
+ .setDisabled(page === tracks.length - 1),
+ new ButtonBuilder()
+ .setCustomId("add")
+ .setLabel("New Track")
+ .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Secondary)
+ .setDisabled(Object.keys(tracks).length >= 24),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Success)
+ .setDisabled(!modified),
+ );
+ if(noTracks) {
+ embed.setDescription("No tracks have been set up yet. Use the button below to add one.\n\n" +
+ createPageIndicator(1, 1, undefined, true)
+ );
+ pageSelect.setDisabled(true);
+ actionSelect.setDisabled(true);
+ pageSelect.addOptions(new StringSelectMenuOptionBuilder()
+ .setLabel("No tracks")
+ .setValue("none")
+ );
+ } else {
+ page = Math.min(page, Object.keys(tracks).length - 1);
+ current = tracks[page]!;
+ const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!);
+ embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
+ `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` +
+ `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` +
+ createVerticalTrack(mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false)) +
+ `\n${createPageIndicator(config.tracks.length, page)}`
+ );
+
+ pageSelect.addOptions(
+ tracks.map((key: ObjectSchema, index) => {
+ return new StringSelectMenuOptionBuilder()
+ .setLabel(ellipsis(key.name, 50))
+ .setDescription(ellipsis(roles.get(key.track[0]!)?.name!, 50))
+ .setValue(index.toString());
+ })
+ );
+
+ }
+
+ await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
+ let i: StringSelectMenuInteraction | ButtonInteraction;
+ try {
+ i = await m.awaitMessageComponent({ time: 300000, filter: (i) => i.user.id === interaction.user.id && i.message.id === m.id && i.channelId === interaction.channelId}) as ButtonInteraction | StringSelectMenuInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+
+ await i.deferUpdate();
+ if (i.isButton()) {
+ switch (i.customId) {
+ case "back": {
+ page--;
+ break;
+ }
+ case "next": {
+ page++;
+ break;
+ }
+ case "add": {
+ const newPage = await editTrack(i, m, roles)
+ if(!newPage) break;
+ tracks.push();
+ page = tracks.length - 1;
+ break;
+ }
+ case "save": {
+ client.database.guilds.write(interaction.guild!.id, {tracks: tracks});
+ modified = false;
+ break;
+ }
+ }
+ } else if (i.isStringSelectMenu()) {
+ switch (i.customId) {
+ case "action": {
+ switch(i.values[0]) {
+ case "edit": {
+ const edited = await editTrack(i, m, roles, current!);
+ if(!edited) break;
+ tracks[page] = edited;
+ modified = true;
+ break;
+ }
+ case "delete": {
+ if(page === 0 && tracks.keys.length - 1 > 0) page++;
+ else page--;
+ tracks.splice(page, 1);
+ break;
+ }
+ }
+ break;
+ }
+ case "page": {
+ page = parseInt(i.values[0]!);
+ break;
+ }
+ }
+ }
+
+ } while (!closed);
+ await interaction.deleteReply()
+}
+
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
+ const member = interaction.member as GuildMember;
+ if (!member.permissions.has("ManageRoles"))
+ return "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/verify.ts b/src/commands/settings/verify.ts
index 0f9f4a0..c440b75 100644
--- a/src/commands/settings/verify.ts
+++ b/src/commands/settings/verify.ts
@@ -1,35 +1,25 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import Discord, {
CommandInteraction,
- Interaction,
Message,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
- ModalSubmitInteraction,
- Role,
ButtonStyle,
- StringSelectMenuBuilder,
- StringSelectMenuComponent,
- TextInputBuilder,
- EmbedBuilder,
- StringSelectMenuInteraction,
- ButtonComponent
+ RoleSelectMenuBuilder,
+ APIMessageComponentEmoji
} from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import client from "../../utils/client.js";
-import { modalInteractionCollector } from "../../utils/dualCollector.js";
+import { getCommandMentionByName } from "../../utils/getCommandDataByName.js";
+import lodash from "lodash";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("verify")
- .setDescription("Manage the role given after typing /verify")
- .addRoleOption((option) =>
- option.setName("role").setDescription("The role to give after verifying").setRequired(false)
- );
+ .setDescription("Manage the role given after a user runs /verify")
+
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
if (!interaction.guild) return;
@@ -38,356 +28,82 @@
ephemeral: true,
fetchReply: true
})) as Message;
- if (interaction.options.get("role")?.role) {
- let role: Role;
- try {
- role = interaction.options.get("role")?.role as Role;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Verify Role")
- .setDescription("The role you provided is not a valid role")
- .setStatus("Danger")
- ]
- });
- }
- role = role as Discord.Role;
- if (role.guild.id !== interaction.guild.id) {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription("You must choose a role in this server")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLES.DELETE")
- ]
- });
- }
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.EDIT")
- .setTitle("Verify Role")
- .setDescription(`Are you sure you want to set the verify role to <@&${role.id}>?`)
- .setColor("Warning")
- .setFailedMessage("No changes were made", "Warning", "GUILD.ROLES.DELETE")
- .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
- });
- const { log, NucleusColors, entry, renderUser, renderRole } = client.logger;
- const data = {
- meta: {
- type: "verifyRoleChanged",
- displayName: "Verify Role Changed",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.green,
- emoji: "CONTROL.BLOCKTICK",
- timestamp: new Date().getTime()
- },
- list: {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user)),
- role: entry(role.id, renderRole(role))
- },
- hidden: {
- guild: interaction.guild.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription("Something went wrong while setting the verify role")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLES.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: []
- });
- }
- }
- let clicks = 0;
- const data = await client.database.guilds.read(interaction.guild.id);
- let role = data.verify.role;
- let timedOut = false;
- while (!timedOut) {
+ let closed = false;
+ let config = await client.database.guilds.read(interaction.guild.id);
+ let data = Object.assign({}, config.verify);
+ do {
+ const selectMenu = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("role")
+ .setPlaceholder("Select a role")
+ );
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Enabled" : "Disabled")
+ .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setDisabled(lodash.isEqual(config.verify, data))
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Verify Role")
+ .setDescription(
+ `Select a role to be given to users after they run ${getCommandMentionByName("verify")}` +
+ `\n\nCurrent role: ${config.verify.role ? `<@&${config.verify.role}>` : "None"}`
+ )
+ .setStatus("Success")
+ .setEmoji("CHANNEL.TEXT.CREATE");
+
await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Role")
- .setDescription(
- role ? `Your verify role is currently set to <@&${role}>` : "You have not set a verify role"
- )
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("clear")
- .setLabel(clicks ? "Click again to confirm" : "Reset role")
- .setEmoji(getEmojiByName(clicks ? "TICKETS.ISSUE" : "CONTROL.CROSS", "id"))
- .setStyle(ButtonStyle.Danger)
- .setDisabled(!role),
- new ButtonBuilder()
- .setCustomId("send")
- .setLabel("Add verify button")
- .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id"))
- .setStyle(ButtonStyle.Primary)
- ])
- ]
+ embeds: [embed],
+ components: [selectMenu, buttons]
});
- let i: MessageComponentInteraction;
+
+ let i;
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id }
});
} catch (e) {
- timedOut = true;
+ closed = true;
continue;
}
- i.deferUpdate();
- if ((i.component as ButtonComponent).customId === "clear") {
- clicks += 1;
- if (clicks === 2) {
- clicks = 0;
- await client.database.guilds.write(interaction.guild.id, null, ["verify.role", "verify.enabled"]);
- role = null;
- }
- } else if ((i.component as ButtonComponent).customId === "send") {
- const verifyMessages = [
- {
- label: "Verify",
- description: "Click the button below to get verified"
- },
- {
- label: "Get verified",
- description: "To get access to the rest of the server, click the button below"
- },
- {
- label: "Ready to verify?",
- description: "Click the button below to verify yourself"
+
+ await i.deferUpdate();
+
+ if(i.isButton()) {
+ switch (i.customId) {
+ case "save": {
+ client.database.guilds.write(interaction.guild.id, {"verify": data} )
+ config = await client.database.guilds.read(interaction.guild.id);
+ data = Object.assign({}, config.verify);
+ break
}
- ];
- let innerTimedOut = false;
- let templateSelected = false;
- while (!innerTimedOut && !templateSelected) {
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Button")
- .setDescription("Select a message template to send in this channel")
- .setFooter({
- text: role ? "" : "You do no have a verify role set so the button will not work."
- })
- .setStatus(role ? "Success" : "Warning")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: [
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([
- new StringSelectMenuBuilder()
- .setOptions(
- verifyMessages.map(
- (
- t: {
- label: string;
- description: string;
- value?: string;
- },
- index
- ) => {
- t.value = index.toString();
- return t as {
- value: string;
- label: string;
- description: string;
- };
- }
- )
- )
- .setCustomId("template")
- .setMaxValues(1)
- .setMinValues(1)
- .setPlaceholder("Select a message template")
- ]),
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setCustomId("back")
- .setLabel("Back")
- .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder().setCustomId("blank").setLabel("Empty").setStyle(ButtonStyle.Secondary),
- new ButtonBuilder()
- .setCustomId("custom")
- .setLabel("Custom")
- .setEmoji(getEmojiByName("TICKETS.OTHER", "id"))
- .setStyle(ButtonStyle.Primary)
- ])
- ]
- });
- let i: MessageComponentInteraction;
- try {
- i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
- } catch (e) {
- innerTimedOut = true;
- continue;
- }
- if ((i.component as StringSelectMenuComponent).customId === "template") {
- i.deferUpdate();
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(verifyMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.label)
- .setDescription(
- verifyMessages[parseInt((i as StringSelectMenuInteraction).values[0]!)]!.description
- )
- .setStatus("Success")
- .setEmoji("CONTROL.BLOCKTICK")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Verify")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("verifybutton")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "blank") {
- i.deferUpdate();
- await interaction.channel!.send({
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Verify")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("verifybutton")
- ])
- ]
- });
- templateSelected = true;
- continue;
- } else if ((i.component as ButtonComponent).customId === "custom") {
- await i.showModal(
- new Discord.ModalBuilder()
- .setCustomId("modal")
- .setTitle("Enter embed details")
- .addComponents(
- new ActionRowBuilder<TextInputBuilder>().addComponents(
- new TextInputBuilder()
- .setCustomId("title")
- .setLabel("Title")
- .setMaxLength(256)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Short)
- ),
- new ActionRowBuilder<TextInputBuilder>().addComponents(
- new TextInputBuilder()
- .setCustomId("description")
- .setLabel("Description")
- .setMaxLength(4000)
- .setRequired(true)
- .setStyle(Discord.TextInputStyle.Paragraph)
- )
- )
- );
- await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Verify Button")
- .setDescription("Modal opened. If you can't see it, click back and try again.")
- .setStatus("Success")
- .setEmoji("GUILD.TICKET.OPEN")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Back")
- .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
- .setStyle(ButtonStyle.Primary)
- .setCustomId("back")
- ])
- ]
- });
- let out;
- try {
- out = await modalInteractionCollector(
- m,
- (m: Interaction) =>
- (m as MessageComponentInteraction | ModalSubmitInteraction).channelId ===
- interaction.channelId,
- (m) => m.customId === "modify"
- );
- } catch (e) {
- innerTimedOut = true;
- continue;
- }
- if (out !== null && out instanceof ModalSubmitInteraction) {
- const title = out.fields.getTextInputValue("title");
- const description = out.fields.getTextInputValue("description");
- await interaction.channel!.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(title)
- .setDescription(description)
- .setStatus("Success")
- .setEmoji("CONTROL.BLOCKTICK")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Verify")
- .setEmoji(getEmojiByName("CONTROL.TICK", "id"))
- .setStyle(ButtonStyle.Success)
- .setCustomId("verifybutton")
- ])
- ]
- });
- templateSelected = true;
- }
+ case "switch": {
+ data.enabled = !data.enabled;
+ break
}
}
} else {
- i.deferUpdate();
- break;
+ data.role = i.values[0]!;
}
- }
- await interaction.editReply({
- embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message closed" })],
- components: []
- });
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
diff --git a/src/commands/settings/welcome.ts b/src/commands/settings/welcome.ts
index e7143fb..7584624 100644
--- a/src/commands/settings/welcome.ts
+++ b/src/commands/settings/welcome.ts
@@ -1,307 +1,263 @@
import { LoadingEmbed } from "../../utils/defaults.js";
import Discord, {
- Channel,
CommandInteraction,
- Message,
+ AutocompleteInteraction,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
- Role,
ButtonStyle,
- AutocompleteInteraction,
- GuildChannel,
- EmbedBuilder
+ APIMessageComponentEmoji,
+ ChannelSelectMenuBuilder,
+ RoleSelectMenuBuilder,
+ RoleSelectMenuInteraction,
+ ChannelSelectMenuInteraction,
+ ButtonInteraction,
+ ModalBuilder,
+ TextInputBuilder,
+ TextInputStyle,
+ ModalSubmitInteraction,
} from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
-import confirmationMessage from "../../utils/confirmationMessage.js";
-import generateKeyValueList from "../../utils/generateKeyValueList.js";
-import { ChannelType } from "discord-api-types/v9";
import getEmojiByName from "../../utils/getEmojiByName.js";
+import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
+import { modalInteractionCollector } from "../../utils/dualCollector.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
.setName("welcome")
.setDescription("Messages and roles sent or given when someone joins the server")
- .addStringOption((option) =>
- option
- .setName("message")
- .setDescription("The message to send when someone joins the server")
- .setAutocomplete(true)
- )
- .addRoleOption((option) =>
- option.setName("role").setDescription("The role given when someone joins the server")
- )
- .addRoleOption((option) =>
- option.setName("ping").setDescription("The role pinged when someone joins the server")
- )
- .addChannelOption((option) =>
- option
- .setName("channel")
- .setDescription("The channel the welcome message should be sent to")
- .addChannelTypes(ChannelType.GuildText)
- );
-const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const { renderRole, renderChannel, log, NucleusColors, entry, renderUser } = client.logger;
- await interaction.reply({
+const callback = async (interaction: CommandInteraction): Promise<void> => {
+ const { renderChannel } = client.logger;
+ const m = await interaction.reply({
embeds: LoadingEmbed,
fetchReply: true,
ephemeral: true
});
- let m: Message;
- if (
- interaction.options.get("role")?.role ||
- interaction.options.get("channel")?.channel ||
- interaction.options.get("message")?.value as string
- ) {
- let role: Role | null;
- let ping: Role | null;
- let channel: Channel | null;
- const message: string | null = interaction.options.get("message")?.value as string | null;
- try {
- role = interaction.options.get("role")?.role as Role | null;
- ping = interaction.options.get("ping")?.role as Role | null;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Welcome Events")
- .setDescription("The role you provided is not a valid role")
- .setStatus("Danger")
- ]
- });
- }
- try {
- channel = interaction.options.get("channel")?.channel as Channel | null;
- } catch {
- return await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setEmoji("GUILD.ROLES.DELETE")
- .setTitle("Welcome Events")
- .setDescription("The channel you provided is not a valid channel")
- .setStatus("Danger")
- ]
- });
- }
- const options: {
- role?: string;
- ping?: string;
- channel?: string;
- message?: string;
- } = {};
-
- if (role) options.role = renderRole(role);
- if (ping) options.ping = renderRole(ping);
- if (channel) options.channel = renderChannel(channel as GuildChannel);
- if (message) options.message = "\n> " + message;
- const confirmation = await new confirmationMessage(interaction)
- .setEmoji("GUILD.ROLES.EDIT")
- .setTitle("Welcome Events")
- .setDescription(generateKeyValueList(options))
- .setColor("Warning")
- .setFailedMessage("Cancelled", "Warning", "GUILD.ROLES.DELETE") //TODO: Actual Message Needed
- .setInverted(true)
- .send(true);
- if (confirmation.cancelled) return;
- if (confirmation.success) {
- try {
- const toChange: {
- "welcome.role"?: string;
- "welcome.ping"?: string;
- "welcome.channel"?: string;
- "welcome.message"?: string;
- } = {};
- if (role) toChange["welcome.role"] = role.id;
- if (ping) toChange["welcome.ping"] = ping.id;
- if (channel) toChange["welcome.channel"] = channel.id;
- if (message) toChange["welcome.message"] = message;
- await client.database.guilds.write(interaction.guild!.id, toChange);
- const list: {
- memberId: ReturnType<typeof entry>;
- changedBy: ReturnType<typeof entry>;
- role?: ReturnType<typeof entry>;
- ping?: ReturnType<typeof entry>;
- channel?: ReturnType<typeof entry>;
- message?: ReturnType<typeof entry>;
- } = {
- memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
- changedBy: entry(interaction.user.id, renderUser(interaction.user))
- };
- if (role) list.role = entry(role.id, renderRole(role));
- if (ping) list.ping = entry(ping.id, renderRole(ping));
- if (channel) list.channel = entry(channel.id, renderChannel(channel as GuildChannel));
- if (message) list.message = entry(message, `\`${message}\``);
- const data = {
- meta: {
- type: "welcomeSettingsUpdated",
- displayName: "Welcome Settings Changed",
- calculateType: "nucleusSettingsUpdated",
- color: NucleusColors.green,
- emoji: "CONTROL.BLOCKTICK",
- timestamp: new Date().getTime()
- },
- list: list,
- hidden: {
- guild: interaction.guild!.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Welcome Events")
- .setDescription("Something went wrong while updating welcome settings")
- .setStatus("Danger")
- .setEmoji("GUILD.ROLES.DELETE")
- ],
- components: []
- });
- }
- } else {
- return interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Welcome Events")
- .setDescription("No changes were made")
- .setStatus("Success")
- .setEmoji("GUILD.ROLES.CREATE")
- ],
- components: []
- });
- }
- }
- let lastClicked = null;
- let timedOut = false;
+ let closed = false;
+ let config = await client.database.guilds.read(interaction.guild!.id);
+ let data = Object.assign({}, config.welcome);
do {
- const config = await client.database.guilds.read(interaction.guild!.id);
- m = (await interaction.editReply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Welcome Events")
- .setDescription(
- `**Message:** ${config.welcome.message ? `\n> ${config.welcome.message}` : "*None set*"}\n` +
- `**Role:** ${
- config.welcome.role
- ? renderRole((await interaction.guild!.roles.fetch(config.welcome.role))!)
- : "*None set*"
- }\n` +
- `**Ping:** ${
- config.welcome.ping
- ? renderRole((await interaction.guild!.roles.fetch(config.welcome.ping))!)
- : "*None set*"
- }\n` +
- `**Channel:** ${
- config.welcome.channel
- ? config.welcome.channel == "dm"
- ? "DM"
- : renderChannel((await interaction.guild!.channels.fetch(config.welcome.channel))!)
- : "*None set*"
- }`
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("switch")
+ .setLabel(data.enabled ? "Enabled" : "Disabled")
+ .setStyle(data.enabled ? ButtonStyle.Success : ButtonStyle.Danger)
+ .setEmoji(getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("message")
+ .setLabel((data.message ? "Change" : "Set") + "Message")
+ .setStyle(ButtonStyle.Primary)
+ .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("channelDM")
+ .setLabel("Send in DMs")
+ .setStyle(ButtonStyle.Primary)
+ .setDisabled(data.channel === "dm"),
+ new ButtonBuilder()
+ .setCustomId("role")
+ .setLabel("Clear Role")
+ .setStyle(ButtonStyle.Danger)
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as APIMessageComponentEmoji),
+ new ButtonBuilder()
+ .setCustomId("save")
+ .setLabel("Save")
+ .setStyle(ButtonStyle.Success)
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setDisabled(
+ data.enabled === config.welcome.enabled &&
+ data.message === config.welcome.message &&
+ data.role === config.welcome.role &&
+ data.ping === config.welcome.ping &&
+ data.channel === config.welcome.channel
)
- .setStatus("Success")
- .setEmoji("CHANNEL.TEXT.CREATE")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-message" ? "Click again to confirm" : "Clear Message")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-message")
- .setDisabled(!config.welcome.message)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-role" ? "Click again to confirm" : "Clear Role")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-role")
- .setDisabled(!config.welcome.role)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-ping" ? "Click again to confirm" : "Clear Ping")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-ping")
- .setDisabled(!config.welcome.ping)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel(lastClicked == "clear-channel" ? "Click again to confirm" : "Clear Channel")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- .setCustomId("clear-channel")
- .setDisabled(!config.welcome.channel)
- .setStyle(ButtonStyle.Danger),
- new ButtonBuilder()
- .setLabel("Set Channel to DM")
- .setCustomId("set-channel-dm")
- .setDisabled(config.welcome.channel == "dm")
- .setStyle(ButtonStyle.Secondary)
- ])
- ]
- })) as Message;
- let i: MessageComponentInteraction;
+ );
+
+ const channelMenu = new ActionRowBuilder<ChannelSelectMenuBuilder>()
+ .addComponents(
+ new ChannelSelectMenuBuilder()
+ .setCustomId("channel")
+ .setPlaceholder("Select a channel to send welcome messages to")
+ );
+ const roleMenu = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("roleToGive")
+ .setPlaceholder("Select a role to give to the member when they join the server")
+ );
+ const pingMenu = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("roleToPing")
+ .setPlaceholder("Select a role to ping when a member joins the server")
+ );
+
+ const embed = new EmojiEmbed()
+ .setTitle("Welcome Settings")
+ .setStatus("Success")
+ .setDescription(
+ `${getEmojiByName(data.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Welcome messages and roles are ${data.enabled ? "enabled" : "disabled"}\n` +
+ `**Welcome message:** ${data.message ?
+ `\n> ` +
+ await convertCurlyBracketString(
+ data.message,
+ interaction.user.id,
+ interaction.user.username,
+ interaction.guild!.name,
+ interaction.guild!.members
+ )
+ : "*None*"}\n` +
+ `**Send message in:** ` + (data.channel ? (data.channel == "dm" ? "DMs" : renderChannel(data.channel)) : `*None set*`) + `\n` +
+ `**Role to ping:** ` + (data.ping ? `<@&${data.ping}>` : `*None set*`) + `\n` +
+ `**Role given on join:** ` + (data.role ? `<@&${data.role}>` : `*None set*`)
+ )
+
+ await interaction.editReply({
+ embeds: [embed],
+ components: [buttons, channelMenu, roleMenu, pingMenu]
+ });
+
+ let i: RoleSelectMenuInteraction | ChannelSelectMenuInteraction | ButtonInteraction;
try {
i = await m.awaitMessageComponent({
- time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
- });
+ filter: (interaction) => interaction.user.id === interaction.user.id,
+ time: 300000
+ }) as RoleSelectMenuInteraction | ChannelSelectMenuInteraction | ButtonInteraction;
} catch (e) {
- timedOut = true;
+ closed = true;
continue;
}
- i.deferUpdate();
- if (i.customId == "clear-message") {
- if (lastClicked == "clear-message") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.message": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-message";
+
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "switch": {
+ await i.deferUpdate();
+ data.enabled = !data.enabled;
+ break;
+ }
+ case "message": {
+ const modal = new ModalBuilder()
+ .setCustomId("modal")
+ .setTitle("Welcome Message")
+ .addComponents(
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex1")
+ .setLabel("Server Info (1/3)")
+ .setPlaceholder(
+ `{serverName} - This server's name\n\n` +
+ `These placeholders will be replaced with the server's name, etc..`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex2")
+ .setLabel("Member Counts (2/3) - {MemberCount:...}")
+ .setPlaceholder(
+ `{:all} - Total member count\n` +
+ `{:humans} - Total non-bot users\n` +
+ `{:bots} - Number of bots\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>().addComponents(
+ new TextInputBuilder()
+ .setCustomId("ex3")
+ .setLabel("Member who joined (3/3) - {member:...}")
+ .setPlaceholder(
+ `{:name} - The members name\n`
+ )
+ .setMaxLength(1)
+ .setRequired(false)
+ .setStyle(TextInputStyle.Paragraph)
+ ),
+ new ActionRowBuilder<TextInputBuilder>()
+ .addComponents(
+ new TextInputBuilder()
+ .setCustomId("message")
+ .setPlaceholder("Enter a message to send when someone joins the server")
+ .setValue(data.message ?? "")
+ .setLabel("Message")
+ .setStyle(TextInputStyle.Paragraph)
+ )
+ )
+ const button = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("back")
+ .setLabel("Back")
+ .setStyle(ButtonStyle.Secondary)
+ .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
+ )
+ await i.showModal(modal)
+ await i.editReply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Welcome Settings")
+ .setDescription("Modal opened. If you can't see it, click back and try again.")
+ .setStatus("Success")
+ ],
+ components: [button]
+ });
+
+ let out: ModalSubmitInteraction | null;
+ try {
+ out = await modalInteractionCollector(m, interaction.user) as ModalSubmitInteraction | null;
+ } catch (e) {
+ console.error(e);
+ out = null;
+ }
+ if(!out) break;
+ data.message = out.fields.getTextInputValue("message");
+ break;
+ }
+ case "save": {
+ await i.deferUpdate();
+ await client.database.guilds.write(interaction.guild!.id, {"welcome": data});
+ config = await client.database.guilds.read(interaction.guild!.id);
+ data = Object.assign({}, config.welcome);
+ break;
+ }
+ case "channelDM": {
+ await i.deferUpdate();
+ data.channel = "dm";
+ break;
+ }
+ case "role": {
+ await i.deferUpdate();
+ data.role = null;
+ break;
+ }
}
- } else if (i.customId == "clear-role") {
- if (lastClicked == "clear-role") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.role": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-role";
+ } else if (i.isRoleSelectMenu()) {
+ await i.deferUpdate();
+ switch(i.customId) {
+ case "roleToGive": {
+ data.role = i.values[0]!;
+ break
+ }
+ case "roleToPing": {
+ data.ping = i.values[0]!;
+ break
+ }
}
- } else if (i.customId == "clear-ping") {
- if (lastClicked == "clear-ping") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.ping": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-ping";
- }
- } else if (i.customId == "clear-channel") {
- if (lastClicked == "clear-channel") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.channel": null
- });
- lastClicked = null;
- } else {
- lastClicked = "clear-channel";
- }
- } else if (i.customId == "set-channel-dm") {
- await client.database.guilds.write(interaction.guild!.id, {
- "welcome.channel": "dm"
- });
- lastClicked = null;
+ } else {
+ await i.deferUpdate();
+ data.channel = i.values[0]!;
}
- } while (!timedOut);
- await interaction.editReply({
- embeds: [new EmbedBuilder(m.embeds[0]!.data).setFooter({ text: "Message timed out" })],
- components: []
- });
+
+ } while (!closed);
+ await interaction.deleteReply()
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageGuild"))
return "You must have the *Manage Server* permission to use this command";
@@ -309,7 +265,7 @@
};
const autocomplete = async (interaction: AutocompleteInteraction): Promise<string[]> => {
- const validReplacements = ["serverName", "memberCount", "memberCount:bots", "memberCount:humans"]
+ const validReplacements = ["serverName", "memberCount:all", "memberCount:bots", "memberCount:humans"]
if (!interaction.guild) return [];
const prompt = interaction.options.getString("message");
const autocompletions = [];
@@ -324,7 +280,7 @@
if (beforeLastOpenBracket !== null) {
if (afterLastOpenBracket !== null) {
for (const replacement of validReplacements) {
- if (replacement.startsWith(afterLastOpenBracket[0].slice(1))) {
+ if (replacement.startsWith(afterLastOpenBracket[0]!.slice(1))) {
autocompletions.push(`${beforeLastOpenBracket[1]}{${replacement}}`);
}
}
@@ -341,4 +297,4 @@
return autocompletions;
};
-export { command, callback, check, autocomplete };
+export { command, callback, check, autocomplete };
\ No newline at end of file
diff --git a/src/commands/tag.ts b/src/commands/tag.ts
index a65947c..6ffecca 100644
--- a/src/commands/tag.ts
+++ b/src/commands/tag.ts
@@ -1,5 +1,4 @@
-import { AutocompleteInteraction, CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
-import { SlashCommandBuilder } from "@discordjs/builders";
+import { AutocompleteInteraction, CommandInteraction, ActionRowBuilder, ButtonBuilder, ButtonStyle, SlashCommandBuilder } from "discord.js";
import client from "../utils/client.js";
import EmojiEmbed from "../utils/generateEmojiEmbed.js";
import { capitalize } from "../utils/generateKeyValueList.js";
@@ -51,10 +50,6 @@
return;
};
-const check = () => {
- return true;
-};
-
const autocomplete = async (interaction: AutocompleteInteraction): Promise<string[]> => {
if (!interaction.guild) return [];
const prompt = interaction.options.getString("tag");
@@ -65,5 +60,4 @@
export { command };
export { callback };
-export { check };
export { autocomplete };
diff --git a/src/commands/tags/create.ts b/src/commands/tags/create.ts
index 788902e..1a1f695 100644
--- a/src/commands/tags/create.ts
+++ b/src/commands/tags/create.ts
@@ -1,6 +1,6 @@
import type Discord from "discord.js";
import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import keyValueList from "../../utils/generateKeyValueList.js";
@@ -106,7 +106,7 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageMessages"))
return "You must have the *Manage Messages* permission to use this command";
diff --git a/src/commands/tags/delete.ts b/src/commands/tags/delete.ts
index 18143d3..4fdb10f 100644
--- a/src/commands/tags/delete.ts
+++ b/src/commands/tags/delete.ts
@@ -1,6 +1,6 @@
import type Discord from "discord.js";
import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import keyValueList from "../../utils/generateKeyValueList.js";
@@ -68,7 +68,7 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as Discord.GuildMember;
if (!member.permissions.has("ManageMessages"))
return "You must have the *Manage Messages* permission to use this command";
diff --git a/src/commands/tags/edit.ts b/src/commands/tags/edit.ts
index e15f9ac..7e297c8 100644
--- a/src/commands/tags/edit.ts
+++ b/src/commands/tags/edit.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction, GuildMember } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import confirmationMessage from "../../utils/confirmationMessage.js";
import keyValueList from "../../utils/generateKeyValueList.js";
@@ -19,9 +19,9 @@
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
if (!interaction.guild) return;
- const name = interaction.options.get("name")?.value as string;
- const value = interaction.options.get("value")?.value as string;
- const newname = interaction.options.get("newname")?.value as string;
+ const name = (interaction.options.get("name")?.value ?? "") as string;
+ const value = (interaction.options.get("value")?.value ?? "") as string;
+ const newname = (interaction.options.get("newname")?.value ?? "") as string;
if (!newname && !value)
return await interaction.reply({
embeds: [
@@ -126,7 +126,7 @@
});
};
-const check = (interaction: CommandInteraction) => {
+const check = (interaction: CommandInteraction, _partial: boolean = false) => {
const member = interaction.member as GuildMember;
if (!member.permissions.has("ManageMessages"))
return "You must have the *Manage Messages* permission to use this command";
diff --git a/src/commands/tags/list.ts b/src/commands/tags/list.ts
index f0563c7..dbb1200 100644
--- a/src/commands/tags/list.ts
+++ b/src/commands/tags/list.ts
@@ -10,7 +10,7 @@
ButtonComponent,
StringSelectMenuBuilder
} from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import client from "../../utils/client.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
@@ -139,13 +139,13 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
continue;
}
- i.deferUpdate();
+ await i.deferUpdate();
if ((i.component as ButtonComponent).customId === "left") {
if (page > 0) page--;
selectPaneOpen = false;
@@ -173,10 +173,6 @@
});
};
-const check = () => {
- return true;
-};
export { command };
export { callback };
-export { check };
diff --git a/src/commands/ticket/close.ts b/src/commands/ticket/close.ts
index d2ffaf9..ff9da8b 100644
--- a/src/commands/ticket/close.ts
+++ b/src/commands/ticket/close.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import close from "../../actions/tickets/delete.js";
const command = (builder: SlashCommandSubcommandBuilder) => builder.setName("close").setDescription("Closes a ticket");
@@ -8,10 +8,5 @@
await close(interaction);
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
diff --git a/src/commands/ticket/create.ts b/src/commands/ticket/create.ts
index 91442b5..2f3ddc6 100644
--- a/src/commands/ticket/create.ts
+++ b/src/commands/ticket/create.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import create from "../../actions/tickets/create.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
@@ -14,10 +14,6 @@
await create(interaction);
};
-const check = () => {
- return true;
-};
export { command };
export { callback };
-export { check };
diff --git a/src/commands/user/about.ts b/src/commands/user/about.ts
index e43ecb7..0eb8580 100644
--- a/src/commands/user/about.ts
+++ b/src/commands/user/about.ts
@@ -10,7 +10,7 @@
APISelectMenuOption,
StringSelectMenuBuilder
} from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import generateKeyValueList from "../../utils/generateKeyValueList.js";
@@ -173,11 +173,8 @@
generateKeyValueList({
member: renderUser(member.user),
id: `\`${member.id}\``,
- roles: `${member.roles.cache.size - 1}` // FIXME
- }) +
- "\n" +
- (s.length > 0 ? s : "*None*") +
- "\n"
+ roles: `${member.roles.cache.size - 1}`
+ }) + "\n" + (s.length > 0 ? s : "*None*") + "\n"
)
)
.setTitle("Roles")
@@ -258,13 +255,13 @@
try {
i = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch {
timedOut = true;
continue;
}
- i.deferUpdate();
+ await i.deferUpdate();
if (i.customId === "left") {
if (page > 0) page--;
selectPaneOpen = false;
@@ -286,11 +283,6 @@
});
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };
export { userAbout };
\ No newline at end of file
diff --git a/src/commands/user/avatar.ts b/src/commands/user/avatar.ts
index 88b3270..da33f51 100644
--- a/src/commands/user/avatar.ts
+++ b/src/commands/user/avatar.ts
@@ -1,6 +1,6 @@
import type { CommandInteraction } from "discord.js";
import type Discord from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import generateKeyValueList from "../../utils/generateKeyValueList.js";
import client from "../../utils/client.js";
@@ -35,10 +35,6 @@
});
};
-const check = () => {
- return true;
-};
export { command };
export { callback };
-export { check };
diff --git a/src/commands/user/role.ts b/src/commands/user/role.ts
new file mode 100644
index 0000000..41820ac
--- /dev/null
+++ b/src/commands/user/role.ts
@@ -0,0 +1,160 @@
+import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, GuildMember, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, UserSelectMenuBuilder, UserSelectMenuInteraction } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
+import client from "../../utils/client.js";
+import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
+import { LoadingEmbed } from "../../utils/defaults.js";
+import getEmojiByName from "../../utils/getEmojiByName.js";
+import listToAndMore from "../../utils/listToAndMore.js"
+
+const { renderUser } = client.logger;
+
+const canEdit = (role: Role, member: GuildMember, me: GuildMember): [string, boolean] => {
+ if(role.position >= me.roles.highest.position ||
+ role.position >= member.roles.highest.position
+ ) return [`~~<@&${role.id}>~~`, false];
+ return [`<@&${role.id}>`, true];
+};
+
+const command = (builder: SlashCommandSubcommandBuilder) =>
+ builder
+ .setName("role")
+ .setDescription("Gives or removes a role from someone")
+ .addUserOption((option) => option.setName("user").setDescription("The user to give or remove the role from"))
+
+const callback = async (interaction: CommandInteraction): Promise<unknown> => {
+ const m = await interaction.reply({ embeds: LoadingEmbed, fetchReply: true, ephemeral: true });
+
+ let member = interaction.options.getMember("user") as GuildMember | null;
+
+ if(!member) {
+ const memberEmbed = new EmojiEmbed()
+ .setTitle("Role")
+ .setDescription(`Please choose a member to edit the roles of.`)
+ .setEmoji("GUILD.ROLES.CREATE")
+ .setStatus("Success");
+ const memberChooser = new ActionRowBuilder<UserSelectMenuBuilder>().addComponents(
+ new UserSelectMenuBuilder()
+ .setCustomId("memberChooser")
+ .setPlaceholder("Select a member")
+ );
+ await interaction.editReply({embeds: [memberEmbed], components: [memberChooser]});
+
+ const filter = (i: UserSelectMenuInteraction) => i.customId === "memberChooser" && i.user.id === interaction.user.id;
+
+ let i: UserSelectMenuInteraction | null;
+ try {
+ i = await m.awaitMessageComponent<5>({ filter, time: 300000});
+ } catch (e) {
+ return;
+ }
+
+ memberEmbed.setDescription(`Editing roles for ${renderUser(i.values[0]!)}`);
+ await i.deferUpdate();
+ await interaction.editReply({ embeds: LoadingEmbed, components: [] })
+ member = await interaction.guild?.members.fetch(i.values[0]!)!;
+
+ }
+
+ let closed = false;
+ let rolesToChange: string[] = [];
+ const roleAdd = new ActionRowBuilder<RoleSelectMenuBuilder>()
+ .addComponents(
+ new RoleSelectMenuBuilder()
+ .setCustomId("roleAdd")
+ .setPlaceholder("Select a role to add")
+ .setMaxValues(25)
+ );
+
+ do {
+
+ const removing = rolesToChange.filter((r) => member!.roles.cache.has(r)).map((r) => canEdit(interaction.guild?.roles.cache.get(r)!, interaction.member as GuildMember, interaction.guild?.members.me!)[0])
+ const adding = rolesToChange.filter((r) => !member!.roles.cache.has(r)).map((r) => canEdit(interaction.guild?.roles.cache.get(r)!, interaction.member as GuildMember, interaction.guild?.members.me!)[0])
+ const embed = new EmojiEmbed()
+ .setTitle("Role")
+ .setDescription(
+ `${getEmojiByName("ICONS.EDIT")} Editing roles for <@${member.id}>\n\n` +
+ `Adding:\n` +
+ `${listToAndMore(adding.length > 0 ? adding : ["None"], 5)}\n` +
+ `Removing:\n` +
+ `${listToAndMore(removing.length > 0 ? removing : ["None"], 5)}\n`
+ )
+ .setEmoji("GUILD.ROLES.CREATE")
+ .setStatus("Success");
+
+ const buttons = new ActionRowBuilder<ButtonBuilder>()
+ .addComponents(
+ new ButtonBuilder()
+ .setCustomId("roleSave")
+ .setLabel("Apply")
+ .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Success),
+ new ButtonBuilder()
+ .setCustomId("roleDiscard")
+ .setLabel("Reset")
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id") as APIMessageComponentEmoji)
+ .setStyle(ButtonStyle.Danger)
+ );
+
+ await interaction.editReply({ embeds: [embed], components: [roleAdd, buttons] });
+
+ let i: RoleSelectMenuInteraction | ButtonInteraction | null;
+ try {
+ i = await m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id, time: 300000 }) as RoleSelectMenuInteraction | ButtonInteraction;
+ } catch (e) {
+ closed = true;
+ continue;
+ }
+
+ i.deferUpdate();
+ if(i.isButton()) {
+ switch(i.customId) {
+ case "roleSave": {
+ const roles = rolesToChange.map((r) => interaction.guild?.roles.cache.get(r)!);
+ await interaction.editReply({ embeds: LoadingEmbed, components: [] });
+ const rolesToAdd: Role[] = [];
+ const rolesToRemove: Role[] = [];
+ for(const role of roles) {
+ if(!canEdit(role, interaction.member as GuildMember, interaction.guild?.members.me!)[1]) continue;
+ if(member.roles.cache.has(role.id)) {
+ rolesToRemove.push(role);
+ } else {
+ rolesToAdd.push(role);
+ }
+ }
+ await member.roles.add(rolesToAdd);
+ await member.roles.remove(rolesToRemove);
+ rolesToChange = [];
+ break;
+ }
+ case "roleDiscard": {
+ rolesToChange = [];
+ await interaction.editReply({ embeds: LoadingEmbed, components: [] });
+ break;
+ }
+ }
+ } else {
+ rolesToChange = i.values;
+ }
+
+ } while (!closed);
+
+};
+
+const check = (interaction: CommandInteraction, partial: boolean = false) => {
+ const member = interaction.member as GuildMember;
+ // Check if the user has manage_roles permission
+ if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
+ if (partial) return true;
+ if (!interaction.guild) return
+ const me = interaction.guild.members.me!;
+ // Check if Nucleus has permission to role
+ if (!me.permissions.has("ManageRoles")) return "I do not have the *Manage Roles* permission";
+ // Allow the owner to role anyone
+ if (member.id === interaction.guild.ownerId) return true;
+ // Allow role
+ return true;
+};
+
+export { command };
+export { callback };
+export { check };
diff --git a/src/commands/user/track.ts b/src/commands/user/track.ts
index 0814cfa..c7f441f 100644
--- a/src/commands/user/track.ts
+++ b/src/commands/user/track.ts
@@ -1,10 +1,11 @@
import { LoadingEmbed } from "../../utils/defaults.js";
-import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, SelectMenuOptionBuilder, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction } from "discord.js";
-import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction, StringSelectMenuOptionBuilder } from "discord.js";
+import type { SlashCommandSubcommandBuilder } from "discord.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import addPlural from "../../utils/plurals.js";
import client from "../../utils/client.js";
+import { createVerticalTrack } from "../../utils/createPageIndicator.js";
const command = (builder: SlashCommandSubcommandBuilder) =>
builder
@@ -12,17 +13,8 @@
.setDescription("Moves a user along a role track")
.addUserOption((option) => option.setName("user").setDescription("The user to manage").setRequired(true));
-const generateFromTrack = (position: number, active: string | boolean, size: number, disabled: string | boolean) => {
- active = active ? "ACTIVE" : "INACTIVE";
- disabled = disabled ? "GREY." : "";
- if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active;
- if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active;
- if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active;
- return "TRACKS.VERTICAL.MIDDLE." + disabled + active;
-};
-
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const { renderUser } = client.logger;
+ const { renderUser, renderRole} = client.logger;
const member = interaction.options.getMember("user") as GuildMember;
const guild = interaction.guild;
if (!guild) return;
@@ -44,10 +36,10 @@
const dropdown = new Discord.StringSelectMenuBuilder()
.addOptions(
config.tracks.map((option, index) => {
- const hasRoleInTrack = option.track.some((element: string) => {
+ const hasRoleInTrack: boolean = option.track.some((element: string) => {
return memberRoles.cache.has(element);
});
- return new SelectMenuOptionBuilder({
+ return new StringSelectMenuOptionBuilder({
default: index === track,
label: option.name,
value: index.toString(),
@@ -68,33 +60,23 @@
(data.retainPrevious
? "When promoted, the user keeps previous roles"
: "Members will lose their current role when promoted") + "\n";
- generated +=
- "\n" +
- data.track
- .map((role, index) => {
- const allow: boolean =
- roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position &&
- !managed;
- allowed.push(!allow);
- return (
- getEmojiByName(
- generateFromTrack(index, memberRoles.cache.has(role), data.track.length, allow)
- ) +
- " " +
- roles.get(role)!.name +
- " [<@&" +
- roles.get(role)!.id +
- ">]"
- );
- })
- .join("\n");
+ for (const role of data.track) {
+ const disabled: boolean =
+ roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position && !managed;
+ allowed.push(!disabled)
+ }
+ generated += "\n" + createVerticalTrack(
+ data.track.map((role) => renderRole(roles.get(role)!)),
+ data.track.map((role) => memberRoles.cache.has(role)),
+ allowed.map((allow) => !allow)
+ );
const selected = [];
for (const position of data.track) {
if (memberRoles.cache.has(position)) selected.push(position);
}
const conflict = data.retainPrevious ? false : selected.length > 1;
let conflictDropdown: StringSelectMenuBuilder[] = [];
- const conflictDropdownOptions: SelectMenuOptionBuilder[] = [];
+ const conflictDropdownOptions: StringSelectMenuOptionBuilder[] = [];
let currentRoleIndex: number = -1;
if (conflict) {
generated += `\n\n${getEmojiByName(`PUNISH.WARN.${managed ? "YELLOW" : "RED"}`)} This user has ${
@@ -106,10 +88,9 @@
"In order to promote or demote this user, you must select which role the member should keep.";
selected.forEach((role) => {
conflictDropdownOptions.push(
- new SelectMenuOptionBuilder({
- label: roles.get(role)!.name,
- value: roles.get(role)!.id
- })
+ new StringSelectMenuOptionBuilder()
+ .setLabel(roles.get(role)!.name)
+ .setValue(roles.get(role)!.id)
);
});
conflictDropdown = [
@@ -169,7 +150,7 @@
try {
component = await m.awaitMessageComponent({
time: 300000,
- filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }
+ filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
});
} catch (e) {
timedOut = true;
@@ -207,9 +188,9 @@
}
};
-const check = async (interaction: CommandInteraction) => {
+const check = async (interaction: CommandInteraction, _partial: boolean = false) => {
const tracks = (await client.database.guilds.read(interaction.guild!.id)).tracks;
- if (tracks.length === 0) throw new Error("This server does not have any tracks");
+ if (tracks.length === 0) return "This server does not have any tracks";
const member = interaction.member as GuildMember;
// Allow the owner to promote anyone
if (member.id === interaction.guild!.ownerId) return true;
@@ -223,8 +204,7 @@
break;
}
// Check if the user has manage_roles permission
- if (!managed && !member.permissions.has("ManageRoles"))
- throw new Error("You do not have the *Manage Roles* permission");
+ if (!managed && !member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
// Allow track
return true;
};
diff --git a/src/commands/verify.ts b/src/commands/verify.ts
index 4fafe69..0dd8b24 100644
--- a/src/commands/verify.ts
+++ b/src/commands/verify.ts
@@ -1,5 +1,5 @@
import type { CommandInteraction } from "discord.js";
-import { SlashCommandBuilder } from "@discordjs/builders";
+import { SlashCommandBuilder } from "discord.js";
import verify from "../reflex/verify.js";
const command = new SlashCommandBuilder().setName("verify").setDescription("Get verified in the server");
@@ -8,10 +8,5 @@
verify(interaction);
};
-const check = () => {
- return true;
-};
-
export { command };
export { callback };
-export { check };