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/Unfinished/all.ts b/src/Unfinished/all.ts
index 9d9e653..eea33f5 100644
--- a/src/Unfinished/all.ts
+++ b/src/Unfinished/all.ts
@@ -5,16 +5,18 @@
ActionRowBuilder,
ButtonBuilder,
SelectMenuBuilder,
- ButtonStyle
+ ButtonStyle,
+ StringSelectMenuBuilder,
+ APIMessageComponentEmoji
} from "discord.js";
-import { SlashCommandSubcommandBuilder } from "@discordjs/builders";
+import { 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";
const command = (builder: SlashCommandSubcommandBuilder) =>
- builder // TODO: DON'T RELEASE THIS
+ builder
.setName("all")
.setDescription("Gives or removes a role from everyone");
@@ -171,8 +173,8 @@
const all = true;
while (true) {
let count = 0;
- const affected = [];
- const members = interaction.guild.members.cache;
+ const affected: GuildMember[] = [];
+ const members = interaction.guild!.members.cache;
if (all) {
members.forEach((member) => {
let applies = true;
@@ -224,8 +226,8 @@
.setStatus("Success")
],
components: [
- new ActionRowBuilder().addComponents([
- new SelectMenuBuilder()
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([
+ new StringSelectMenuBuilder()
.setOptions(
filters.map((f, index) => ({
label: (f.inverted ? "(Not) " : "") + f.name,
@@ -237,18 +239,18 @@
.setCustomId("select")
.setPlaceholder("Remove a filter")
]),
- new ActionRowBuilder().addComponents([
+ new ActionRowBuilder<ButtonBuilder>().addComponents([
new ButtonBuilder()
.setLabel("Apply")
.setStyle(ButtonStyle.Primary)
.setCustomId("apply")
- .setEmoji(client.emojis.cache.get(getEmojiByName("CONTROL.TICK", "id")))
+ .setEmoji(client.emojis.cache.get(getEmojiByName("CONTROL.TICK", "id"))! as APIMessageComponentEmoji)
.setDisabled(affected.length === 0),
new ButtonBuilder()
.setLabel("Add filter")
.setStyle(ButtonStyle.Primary)
.setCustomId("add")
- .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.FILTER", "id")))
+ .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.FILTER", "id"))! as APIMessageComponentEmoji)
.setDisabled(filters.length >= 25)
])
]
@@ -260,12 +262,12 @@
const check = async (interaction: CommandInteraction) => {
const member = interaction.member as GuildMember;
- const me = interaction.guild.me!;
- if (!me.permissions.has("MANAGE_ROLES")) throw new Error("I do not have the *Manage Roles* permission");
+ const me = interaction.guild!.members.me!;
+ 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;
+ if (member.id === interaction.guild!.ownerId) return true;
// Check if the user has manage_roles permission
- if (!member.permissions.has("MANAGE_ROLES")) throw new Error("You do not have the *Manage Roles* permission");
+ if (!member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
// Allow role
return true;
};
diff --git a/src/Unfinished/categorisationTest.ts b/src/Unfinished/categorizationTest.ts
similarity index 72%
rename from src/Unfinished/categorisationTest.ts
rename to src/Unfinished/categorizationTest.ts
index dc38dfe..ff2d66b 100644
--- a/src/Unfinished/categorisationTest.ts
+++ b/src/Unfinished/categorizationTest.ts
@@ -1,28 +1,30 @@
import { LoadingEmbed } from "../utils/defaults.js";
-import { CommandInteraction, GuildChannel, ActionRowBuilder, ButtonBuilder, SelectMenuBuilder, ButtonStyle } from "discord.js";
-import { SlashCommandBuilder } from "@discordjs/builders";
+import { CommandInteraction, GuildChannel, ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, StringSelectMenuBuilder, APIMessageComponentEmoji } from "discord.js";
+import { SlashCommandBuilder } from "discord.js";
import EmojiEmbed from "../utils/generateEmojiEmbed.js";
import client from "../utils/client.js";
import addPlural from "../utils/plurals.js";
import getEmojiByName from "../utils/getEmojiByName.js";
-const command = new SlashCommandBuilder().setName("categorise").setDescription("Categorises your servers channels");
+const command = new SlashCommandBuilder().setName("categorize").setDescription("Categorizes your servers channels");
const callback = async (interaction: CommandInteraction): Promise<unknown> => {
- const channels = interaction.guild.channels.cache.filter((c) => c.type !== "GUILD_CATEGORY");
- const categorised = {};
+ const channels = interaction.guild!.channels.cache.filter((c) => c.type !== ChannelType.GuildCategory);
+ const categorized = {};
await interaction.reply({ embeds: LoadingEmbed, ephemeral: true });
const predicted = {};
const types = {
- general: ["general", "muted", "main", "topic", "discuss"],
+ important: ["rule", "announcement", "alert", "info"],
+ general: ["general", "main", "topic", "discuss"],
commands: ["bot", "command", "music"],
- images: ["pic", "selfies", "image"],
- nsfw: ["porn", "nsfw", "sex"],
- links: ["links"],
- advertising: ["ads", "advert", "server", "partner"],
- staff: ["staff", "mod", "admin"],
- spam: ["spam"],
- other: ["random"]
+ images: ["pic", "selfies", "image", "gallery", "meme", "media"],
+ nsfw: ["porn", "nsfw", "sex", "lewd", "fetish"],
+ links: ["link"],
+ advertising: ["ads", "advert", "partner", "bump"],
+ staff: ["staff", "mod", "admin", "helper", "train"],
+ spam: ["spam", "count"],
+ logs: ["log"],
+ other: ["random", "starboard"],
};
for (const c of channels.values()) {
for (const type in types) {
@@ -38,14 +40,14 @@
for (const c of channels) {
// convert channel to a channel if its a string
let channel: string | GuildChannel;
- if (typeof c === "string") channel = interaction.guild.channels.cache.get(channel as string).id;
+ if (typeof c === "string") channel = interaction.guild!.channels.cache.get(c as string)!.id;
else channel = (c[0] as unknown as GuildChannel).id;
console.log(channel);
if (!predicted[channel]) predicted[channel] = [];
m = await interaction.editReply({
embeds: [
new EmojiEmbed()
- .setTitle("Categorise")
+ .setTitle("Categorize")
.setDescription(
`Select all types that apply to <#${channel}>.\n\n` +
`${addPlural(predicted[channel].length, "Suggestion")}: ${predicted[channel].join(", ")}`
@@ -54,8 +56,8 @@
.setStatus("Success")
],
components: [
- new ActionRowBuilder().addComponents([
- new SelectMenuBuilder()
+ new ActionRowBuilder<StringSelectMenuBuilder>().addComponents([
+ new StringSelectMenuBuilder()
.setCustomId("selected")
.setMaxValues(Object.keys(types).length)
.setMinValues(1)
@@ -67,18 +69,18 @@
}))
)
]),
- new ActionRowBuilder().addComponents([
+ new ActionRowBuilder<ButtonBuilder>().addComponents([
new ButtonBuilder()
.setLabel("Accept Suggestion")
.setCustomId("accept")
.setStyle(ButtonStyle.Success)
.setDisabled(predicted[channel].length === 0)
- .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.TICK", "id"))),
+ .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.TICK", "id")) as APIMessageComponentEmoji),
new ButtonBuilder()
.setLabel('Use "Other"')
.setCustomId("reject")
.setStyle(ButtonStyle.Secondary)
- .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.CROSS", "id")))
+ .setEmoji(client.emojis.cache.get(getEmojiByName("ICONS.CROSS", "id")) as APIMessageComponentEmoji)
])
]
});
@@ -86,13 +88,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) {
return await interaction.editReply({
embeds: [
new EmojiEmbed()
- .setTitle("Categorise")
+ .setTitle("Categorize")
.setEmoji("CHANNEL.CATEGORY.DELETE")
.setStatus("Danger")
.setDescription(
@@ -105,7 +107,7 @@
]
});
}
- i.deferUpdate();
+ await i.deferUpdate();
let selected;
if (i.customId === "select") {
selected = i.values;
@@ -116,9 +118,9 @@
if (i.customId === "reject") {
selected = ["other"];
}
- categorised[channel] = selected;
+ categorized[channel] = selected;
}
- console.log(categorised);
+ console.log(categorized);
};
const check = () => {
diff --git a/src/actions/createModActionTicket.ts b/src/actions/createModActionTicket.ts
index d6e9cd9..d86c14a 100644
--- a/src/actions/createModActionTicket.ts
+++ b/src/actions/createModActionTicket.ts
@@ -1,4 +1,4 @@
-import { getCommandMentionByName } from './../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from './../utils/getCommandDataByName.js';
import Discord, { ActionRowBuilder, ButtonBuilder, OverwriteType, ChannelType, ButtonStyle } from "discord.js";
import EmojiEmbed from "../utils/generateEmojiEmbed.js";
import getEmojiByName from "../utils/getEmojiByName.js";
@@ -86,7 +86,7 @@
`**Support type:** ${customReason ? customReason : "Appeal submission"}\n` +
(reason !== null ? `**Reason:**\n> ${reason}\n` : "") +
`**Ticket ID:** \`${c.id}\`\n` +
- `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.`
+ `Type ${getCommandMentionByName("ticket/close")} to close this ticket.`
)
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
@@ -131,7 +131,7 @@
`**Support type:** ${customReason ? customReason : "Appeal submission"}\n` +
(reason !== null ? `**Reason:**\n> ${reason}\n` : "") +
`**Ticket ID:** \`${c.id}\`\n` +
- `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.`
+ `Type ${getCommandMentionByName("ticket/close")} to close this ticket.`
)
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
@@ -157,12 +157,12 @@
calculateType: "ticketUpdate",
color: NucleusColors.green,
emoji: "GUILD.TICKET.OPEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
ticketFor: entry(user.id, renderUser(user)),
createdBy: entry(createdBy.id, renderUser(createdBy)),
- created: entry((new Date().getTime()).toString(), renderDelta(new Date().getTime())),
+ created: entry((Date.now()).toString(), renderDelta(Date.now())),
ticketChannel: entry(c.id, renderChannel(c))
},
hidden: {
diff --git a/src/actions/roleMenu.ts b/src/actions/roleMenu.ts
index 7056fe6..be58d99 100644
--- a/src/actions/roleMenu.ts
+++ b/src/actions/roleMenu.ts
@@ -30,6 +30,36 @@
interaction: CommandInteraction | ButtonInteraction | ContextMenuCommandInteraction;
}
+interface ObjectSchema {
+ name: string;
+ description: string;
+ min: number;
+ max: number;
+ options: {
+ name: string;
+ description: string | null;
+ role: string;
+ }[];
+}
+
+export const configToDropdown = (placeholder: string, currentPageData: ObjectSchema, selectedRoles?: string[]): ActionRowBuilder<StringSelectMenuBuilder> => {
+ return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
+ new StringSelectMenuBuilder()
+ .setCustomId("roles")
+ .setPlaceholder(placeholder)
+ .setMinValues(currentPageData.min)
+ .setMaxValues(currentPageData.max)
+ .addOptions(currentPageData.options.map((option: {name: string; description: string | null; role: string;}) => {
+ const builder = new StringSelectMenuOptionBuilder()
+ .setLabel(option.name)
+ .setValue(option.role)
+ .setDefault(selectedRoles ? selectedRoles.includes(option.role) : false);
+ if (option.description) builder.setDescription(option.description);
+ return builder;
+ }))
+ )
+}
+
export async function callback(interaction: CommandInteraction | ButtonInteraction) {
if (!interaction.member) return;
if (!interaction.guild) return;
@@ -56,7 +86,7 @@
],
ephemeral: true
});
- const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true });
+ const m = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
if (config.roleMenu.allowWebUI) { // TODO: Make rolemenu web ui
const loginMethods: {webUI: boolean} = {
webUI: false
@@ -75,7 +105,7 @@
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let valid = false;
while (!valid) {
- itt += 1;
+ itt ++;
code = "";
for (let i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
@@ -83,7 +113,7 @@
if (code in client.roleMenu) continue;
if (itt > 1000) {
itt = 0;
- length += 1;
+ length ++;
continue;
}
valid = true;
@@ -124,9 +154,10 @@
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.channelId === interaction.channelId && i.message.id === m.id}
});
} catch (e) {
+ console.log(e);
return;
}
component.deferUpdate();
@@ -151,8 +182,7 @@
`**${currentPageData.name}**\n` +
`> ${currentPageData.description}\n\n` +
(currentPageData.min === currentPageData.max ? `Select ${addPlural(currentPageData.min, "role")}` :
- `Select between ${currentPageData.min} and ${currentPageData.max} roles` + (
- currentPageData.min === 0 ? ` or press next` : "")) + "\n\n" +
+ `Select between ${currentPageData.min} and ${currentPageData.max} roles then press next`) + "\n\n" +
createPageIndicator(maxPage, page)
)
.setStatus("Success")
@@ -175,21 +205,7 @@
.setCustomId("done")
.setDisabled(!complete)
),
- new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
- new StringSelectMenuBuilder()
- .setCustomId("roles")
- .setPlaceholder("Select...")
- .setMinValues(currentPageData.min)
- .setMaxValues(currentPageData.max)
- .addOptions(currentPageData.options.map((option) => {
- const builder = new StringSelectMenuOptionBuilder()
- .setLabel(option.name)
- .setValue(option.role)
- .setDefault(selectedRoles[page]!.includes(option.role));
- if (option.description) builder.setDescription(option.description);
- return builder;
- }))
- )
+ configToDropdown("Select...", currentPageData, selectedRoles[page])
];
await interaction.editReply({
embeds: [embed],
@@ -199,9 +215,10 @@
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) {
+ console.log(e);
return;
}
component.deferUpdate();
diff --git a/src/actions/tickets/create.ts b/src/actions/tickets/create.ts
index 3c2dd2c..237790e 100644
--- a/src/actions/tickets/create.ts
+++ b/src/actions/tickets/create.ts
@@ -3,7 +3,7 @@
import client from "../../utils/client.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
-import { getCommandMentionByName } from "../../utils/getCommandMentionByName.js";
+import { getCommandMentionByName } from "../../utils/getCommandDataByName.js";
function capitalize(s: string) {
s = s.replace(/([A-Z])/g, " $1");
@@ -106,7 +106,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) {
return;
@@ -225,7 +225,7 @@
chosenType !== null ? emoji + " " + capitalize(chosenType) : "General"
}\n` +
`**Ticket ID:** \`${c.id}\`\n${content ?? ""}\n` +
- `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.`
+ `Type ${getCommandMentionByName("ticket/close")} to close this ticket.`
)
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
@@ -257,7 +257,7 @@
type: Discord.ChannelType.PrivateThread,
reason: "Creating ticket"
}) as Discord.PrivateThreadChannel;
- c.members.add(interaction.member!.user.id); // TODO: When a thread is used, and a support role is added, automatically set channel permissions
+ c.members.add(interaction.member!.user.id);
try {
await c.send({
content:
@@ -289,7 +289,7 @@
chosenType !== null ? emoji + " " + capitalize(chosenType) : "General"
}\n` +
`**Ticket ID:** \`${c.id}\`\n${content ?? ""}\n` +
- `Type ${await getCommandMentionByName("ticket/close")} to close this ticket.`
+ `Type ${getCommandMentionByName("ticket/close")} to close this ticket.`
)
.setStatus("Success")
.setEmoji("GUILD.TICKET.OPEN")
@@ -323,11 +323,11 @@
calculateType: "ticketUpdate",
color: NucleusColors.green,
emoji: "GUILD.TICKET.OPEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
ticketFor: entry(interaction.member!.user.id, renderUser(interaction.member!.user! as Discord.User)),
- created: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ created: entry(Date.now(), renderDelta(Date.now())),
ticketChannel: entry(c.id, renderChannel(c))
},
hidden: {
diff --git a/src/actions/tickets/delete.ts b/src/actions/tickets/delete.ts
index 3263580..990b360 100644
--- a/src/actions/tickets/delete.ts
+++ b/src/actions/tickets/delete.ts
@@ -1,15 +1,15 @@
-import { getCommandMentionByName } from '../../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from '../../utils/getCommandDataByName.js';
import Discord, { ActionRowBuilder, ButtonBuilder, ButtonInteraction, PrivateThreadChannel, TextChannel, ButtonStyle, CategoryChannel } from "discord.js";
import client from "../../utils/client.js";
import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
import getEmojiByName from "../../utils/getEmojiByName.js";
import { preloadPage } from '../../utils/createTemporaryStorage.js';
+import { LoadingEmbed } from '../../utils/defaults.js';
export default async function (interaction: Discord.CommandInteraction | ButtonInteraction) {
if (!interaction.guild) return;
const config = await client.database.guilds.read(interaction.guild.id);
const { log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger;
-
const ticketChannel = config.tickets.category;
if (!("parent" in interaction.channel!)) {
return await interaction.reply({
@@ -50,7 +50,7 @@
calculateType: "ticketUpdate",
color: NucleusColors.red,
emoji: "GUILD.TICKET.CLOSE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
ticketFor: entry(
@@ -58,7 +58,7 @@
renderUser((await interaction.guild.members.fetch(uID!)).user)
),
closedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)),
- closed: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ closed: entry(Date.now(), renderDelta(Date.now())),
ticketChannel: entry(channel.id, channel.name)
},
hidden: {
@@ -69,8 +69,9 @@
await channel.delete();
} else if (status === "Active") {
- // Close the ticket
-
+ await interaction.reply({embeds: LoadingEmbed, fetchReply: true});
+ // Archive the ticket
+ await interaction.channel.fetch()
if (channel.isThread()) {
channel.setName(`${channel.name.replace("Active", "Archived")}`);
channel.members.remove(channel.name.split(" - ")[1]!);
@@ -80,14 +81,14 @@
await channel.permissionOverwrites.delete(channel.topic!.split(" ")[0]!);
}
preloadPage(interaction.channel.id, "privacy", "2")
- await interaction.reply({
+ const hasPremium = await client.database.premium.hasPremium(interaction.guild.id);
+ await interaction.editReply({
embeds: [
new EmojiEmbed()
.setTitle("Archived Ticket")
- .setDescription(`This ticket has been Archived. Type ${await getCommandMentionByName("ticket/close")} to delete it.` +
- await client.database.premium.hasPremium(interaction.guild.id) ?
- `\n\nFor more info on transcripts, check ${await getCommandMentionByName("privacy")}` :
- "")
+ .setDescription(`This ticket has been Archived. Type ${getCommandMentionByName("ticket/close")} to delete it.\n` +
+ hasPremium ? ("Creating a transcript will delete all messages in this ticket" +
+ `\n\nFor more info on transcripts, check ${getCommandMentionByName("privacy")}`): "")
.setStatus("Warning")
.setEmoji("GUILD.TICKET.ARCHIVED")
],
@@ -100,7 +101,7 @@
.setCustomId("closeticket")
.setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
].concat(
- await client.database.premium.hasPremium(interaction.guild.id)
+ hasPremium
? [
new ButtonBuilder()
.setLabel("Create Transcript and Delete")
@@ -120,7 +121,7 @@
calculateType: "ticketUpdate",
color: NucleusColors.yellow,
emoji: "GUILD.TICKET.ARCHIVED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
ticketFor: entry(
@@ -128,7 +129,7 @@
renderUser((await interaction.guild.members.fetch(uID!)).user)
),
archivedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as Discord.User)),
- archived: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ archived: entry(Date.now(), renderDelta(Date.now())),
ticketChannel: entry(channel.id, renderChannel(channel))
},
hidden: {
@@ -183,12 +184,12 @@
calculateType: "ticketUpdate",
color: NucleusColors.red,
emoji: "GUILD.TICKET.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
ticketFor: entry(member, renderUser(member)),
deletedBy: entry(null, "Member left server"),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ deleted: entry(Date.now(), renderDelta(Date.now())),
ticketsDeleted: deleted
},
hidden: {
@@ -198,4 +199,4 @@
log(data);
}
-export { purgeByUser };
\ No newline at end of file
+export { purgeByUser };
diff --git a/src/api/index.ts b/src/api/index.ts
index c24327d..9676194 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -57,7 +57,7 @@
calculateType: "guildMemberVerify",
color: NucleusColors.green,
emoji: "CONTROL.BLOCKTICK",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
member: entry(member.id, renderUser(member.user)),
@@ -149,6 +149,49 @@
return res.sendStatus(404);
});
+ app.get("/transcript/:code/human", jsonParser, async function (req: express.Request, res: express.Response) {
+ const code = req.params.code;
+ if (code === undefined) return res.status(400).send("No code provided");
+ const entry = await client.database.transcripts.read(code);
+ if (entry === null) return res.status(404).send("Could not find a transcript by that code");
+ // Convert to a human readable format
+ const data = client.database.transcripts.toHumanReadable(entry);
+ res.attachment(`${code}.txt`);
+ res.type("txt");
+ return res.status(200).send(data);
+ });
+
+ app.get("/transcript/:code", jsonParser, async function (req: express.Request, res: express.Response) {
+ const code = req.params.code;
+ if (code === undefined) return res.status(400).send("No code provided");
+ const entry = await client.database.transcripts.read(code);
+ if (entry === null) return res.status(404).send("Could not find a transcript by that code");
+ // Convert to a human readable format
+ return res.status(200).send(entry);
+ });
+
+ app.get("/channels/:id", jsonParser, async function (req: express.Request, res: express.Response) {
+ const id = req.params.id;
+ if (id === undefined) return res.status(400).send("No id provided");
+ const channel = await client.channels.fetch(id);
+ if (channel === null) return res.status(404).send("Could not find a channel by that id");
+ if (channel.isDMBased()) return res.status(400).send("Cannot get a DM channel");
+ return res.status(200).send(channel.name);
+ });
+
+ app.get("/users/:id", jsonParser, async function (req: express.Request, res: express.Response) {
+ const id = req.params.id;
+ if (id === undefined) return res.status(400).send("No id provided");
+ let user;
+ try {
+ user = await client.users.fetch(id);
+ } catch (e) {
+ console.log(e)
+ return res.status(404).send("Could not find a user by that id");
+ }
+ return res.status(200).send(user.username);
+ });
+
app.listen(port);
};
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 };
diff --git a/src/config/default.json b/src/config/default.json
deleted file mode 100644
index 8e4197c..0000000
--- a/src/config/default.json
+++ /dev/null
@@ -1,100 +0,0 @@
-{
- "id": "default",
- "version": 1,
- "singleEventNotifications": {
- "statsChannelDeleted": false
- },
- "filters": {
- "images": {
- "NSFW": false,
- "size": false
- },
- "malware": false,
- "wordFilter": {
- "enabled": false,
- "words": {
- "strict": [],
- "loose": []
- }
- },
- "invite": {
- "enabled": false,
- "allowed": {
- "users": [],
- "roles": [],
- "channels": []
- }
- },
- "pings": {
- "mass": 5,
- "everyone": true,
- "roles": true
- }
- },
- "welcome": {
- "enabled": false,
- "role": null,
- "ping": null,
- "channel": null,
- "message": null
- },
- "stats": {},
- "logging": {
- "logs": {
- "enabled": true,
- "channel": null,
- "toLog": "3fffff"
- },
- "staff": {
- "channel": null
- },
- "attachments": {
- "channel": null,
- "saved": {}
- }
- },
- "verify": {
- "enabled": false,
- "role": null
- },
- "tickets": {
- "enabled": false,
- "category": null,
- "types": "3f",
- "customTypes": null,
- "useCustom": false,
- "supportRole": null,
- "maxTickets": 5
- },
- "moderation": {
- "mute": {
- "timeout": true,
- "role": null,
- "text": null,
- "link": null
- },
- "kick": {
- "text": null,
- "link": null
- },
- "ban": {
- "text": null,
- "link": null
- },
- "softban": {
- "text": null,
- "link": null
- },
- "warn": {
- "text": null,
- "link": null
- },
- "nickname": {
- "text": null,
- "link": null
- }
- },
- "tracks": [],
- "roleMenu": [],
- "tags": {}
-}
diff --git a/src/config/emojis.json b/src/config/emojis.json
index 26d4c98..ecf1858 100644
--- a/src/config/emojis.json
+++ b/src/config/emojis.json
@@ -1,6 +1,8 @@
{
"NUCLEUS": {
"LOGO": "953040840945721385",
+ "PREMIUMACTIVATE": "a1067536222764925068",
+ "PREMIUM": "1067928702027042876",
"LOADING": "a946346549271732234",
"INFO": {
"HELP": "751751467014029322",
@@ -22,9 +24,11 @@
"FILTER": "990242059451514902",
"ATTACHMENT": "997570687193587812",
"LOGGING": "999613304446144562",
+ "SAVE": "1065722246322200586",
+ "REORDER": "1069323453909454890",
"NOTIFY": {
"ON": "1000726394579464232",
- "OFF": "1000726363495477368"
+ "OFF": "1078058136092541008"
},
"OPP": {
"ADD": "837355918831124500",
@@ -94,9 +98,8 @@
"TITLEUPDATE": "729763053620691044",
"TOPICUPDATE": "729763053477953536",
"SLOWMODE": {
- "ON": "777138171301068831",
- "OFF": "777138171447869480",
- "// TODO": "Make these green and red respectively"
+ "ON": "973616021304913950",
+ "OFF": "777138171447869480"
},
"NSFW": {
"ON": "729064531208175736",
@@ -210,6 +213,12 @@
"STOP": "853519660116738078"
}
},
+ "SETTINGS": {
+ "STATS": {
+ "GREEN": "752214059159650396",
+ "RED": "1065677252630675556"
+ }
+ },
"GUILD": {
"RED": "959779988264079361",
"YELLOW": "729763053352124529",
@@ -219,8 +228,10 @@
"EDIT": "729066518549233795",
"DELETE": "953035210121953320"
},
- "GRAPHS": "752214059159650396",
- "SETTINGS": "752570111063228507",
+ "SETTINGS": {
+ "GREEN": "752570111063228507",
+ "RED": "1068607393728049253"
+ },
"ICONCHANGE": "729763053612302356",
"TICKET": {
"OPEN": "853245836331188264",
@@ -338,7 +349,7 @@
"TOP": {
"ACTIVE": "963122664648630293",
"INACTIVE": "963122659862917140",
- "GREY": {
+ "GRAY": {
"ACTIVE": "963123505052934144",
"INACTIVE": "963123495221469194"
}
@@ -346,7 +357,7 @@
"MIDDLE": {
"ACTIVE": "963122679332880384",
"INACTIVE": "963122673246937199",
- "GREY": {
+ "GRAY": {
"ACTIVE": "963123517702955018",
"INACTIVE": "963123511927390329"
}
@@ -354,7 +365,7 @@
"BOTTOM": {
"ACTIVE": "963122691752218624",
"INACTIVE": "963122685691453552",
- "GREY": {
+ "GRAY": {
"ACTIVE": "963123529988059187",
"INACTIVE": "963123523742748742"
}
@@ -363,10 +374,20 @@
"SINGLE": {
"ACTIVE": "963361162215424060",
"INACTIVE": "963361431758176316",
- "GREY": {
+ "GRAY": {
"ACTIVE": "963361204695334943",
"INACTIVE": "963361200828198952"
}
}
+ },
+ "COLORS": {
+ "RED": "875822912802803754",
+ "ORANGE": "875822913104785418",
+ "YELLOW": "875822913079611402",
+ "GREEN": "875822913213841418",
+ "BLUE": "875822912777637889",
+ "PURPLE": "875822913213841419",
+ "PINK": "875822913088020541",
+ "GRAY": "875822913117368340"
}
}
diff --git a/src/config/format.ts b/src/config/format.ts
index 713a233..e32bef6 100644
--- a/src/config/format.ts
+++ b/src/config/format.ts
@@ -1,8 +1,8 @@
+
import fs from "fs";
-// @ts-expect-error
import * as readLine from "node:readline/promises";
-const defaultDict: Record<string, string | string[] | boolean> = {
+const defaultDict: Record<string, string | string[] | boolean | Record<string, string>> = {
developmentToken: "Your development bot token (Used for testing in one server, rather than production)",
developmentGuildID: "Your development guild ID",
enableDevelopment: true,
@@ -15,7 +15,17 @@
userContextFolder: "Your built user context folder (usually dist/context/users)",
verifySecret:
"If using verify, enter a code here which matches the secret sent back by your website. You can use a random code if you do not have one already. (Optional)",
- mongoUrl: "Your Mongo connection string, e.g. mongodb://127.0.0.1:27017",
+ mongoUsername: "Your Mongo username (optional)",
+ mongoPassword: "Your Mongo password (optional)",
+ mongoDatabase: "Your Mongo database name (optional, e.g. Nucleus)",
+ mongoHost: "Your Mongo host (optional, e.g. localhost:27017)",
+ mongoOptions: {
+ username: "",
+ password: "",
+ database: "",
+ host: "",
+ authSource: "",
+ },
baseUrl: "Your website where buttons such as Verify and Role menu will link to, e.g. https://example.com/",
pastebinApiKey: "An API key for pastebin (optional)",
pastebinUsername: "Your pastebin username (optional)",
@@ -51,18 +61,27 @@
// }
}
- let json;
+ let json: typeof defaultDict;
let out = true;
try {
- json = JSON.parse(fs.readFileSync("./src/config/main.json", "utf8"));
+ json = await import("./main.js") as unknown as typeof defaultDict;
} catch (e) {
- console.log("\x1b[31m⚠ No main.json found, creating one.");
- console.log(" \x1b[2mYou can edit src/config/main.json directly using template written to the file.\x1b[0m\n");
+ console.log("\x1b[31m⚠ No main.ts found, creating one.");
+ console.log(" \x1b[2mYou can edit src/config/main.ts directly using template written to the file.\x1b[0m\n");
out = false;
- json = {};
+ json = {} as typeof defaultDict;
}
+
+ if (Object.keys(json).length) {
+ if (json["token"] === defaultDict["token"] || json["developmentToken"] === defaultDict["developmentToken"]) {
+ console.log("\x1b[31m⚠ No main.ts found, creating one.");
+ console.log(" \x1b[2mYou can edit src/config/main.ts directly using template written to the file.\x1b[0m\n");
+ json = {};
+ }
+ }
+
for (const key in defaultDict) {
- if (!json[key]) {
+ if (Object.keys(json).includes(key)) {
if (walkthrough) {
switch (key) {
case "enableDevelopment": {
@@ -88,18 +107,20 @@
json[key] = toWrite;
break;
}
+ case "mongoOptions": {
+ break;
+ }
default: {
json[key] = await getInput(`\x1b[36m${key} \x1b[0m(\x1b[35m${defaultDict[key]}\x1b[0m) > `);
}
}
} else {
- json[key] = defaultDict[key];
+ json[key] = defaultDict[key]!;
}
}
}
- if (walkthrough && !json.mongoUrl) json.mongoUrl = "mongodb://127.0.0.1:27017";
- if (!json.mongoUrl.endsWith("/")) json.mongoUrl += "/";
- if (!json.baseUrl.endsWith("/")) json.baseUrl += "/";
+ if (walkthrough && !(json["mongoUrl"] ?? false)) json["mongoUrl"] = "mongodb://127.0.0.1:27017";
+ if (!((json["baseUrl"] as string | undefined) ?? "").endsWith("/")) (json["baseUrl"] as string) += "/";
let hosts;
try {
hosts = fs.readFileSync("/etc/hosts", "utf8").toString().split("\n");
@@ -108,16 +129,23 @@
"\x1b[31m⚠ No /etc/hosts found. Please ensure the file exists and is readable. (Windows is not supported, Mac and Linux users should not experience this error)"
);
}
- let localhost = hosts.find((line) => line.split(" ")[1] === "localhost");
+ let localhost: string | undefined = hosts.find((line) => line.split(" ")[1] === "localhost");
if (localhost) {
localhost = localhost.split(" ")[0];
} else {
localhost = "127.0.0.1";
}
- json.mongoUrl = json.mongoUrl.replace("localhost", localhost);
- json.baseUrl = json.baseUrl.replace("localhost", localhost);
+ json["mongoUrl"] = (json["mongoUrl"]! as string).replace("localhost", localhost!);
+ json["baseUrl"] = (json["baseUrl"]! as string).replace("localhost", localhost!);
+ json["mongoOptions"] = {
+ username: json["username"] as string,
+ password: json["password"] as string,
+ database: json["database"] as string,
+ host: json["host"] as string,
+ authSource: json["authSource"] as string,
+ };
- fs.writeFileSync("./src/config/main.json", JSON.stringify(json, null, 4));
+ fs.writeFileSync("./src/config/main.ts", "export default " + JSON.stringify(json, null, 4) + ";");
if (walkthrough) {
console.log("\x1b[32m✓ All properties added.\x1b[0m");
diff --git a/src/config/main.d.ts b/src/config/main.d.ts
new file mode 100644
index 0000000..6549234
--- /dev/null
+++ b/src/config/main.d.ts
@@ -0,0 +1,26 @@
+declare const config: {
+ developmentToken: string,
+ developmentGuildID: string,
+ enableDevelopment: boolean,
+ token: string,
+ managementGuildID: string,
+ owners: string[],
+ commandsFolder: string,
+ eventsFolder: string,
+ messageContextFolder: string,
+ userContextFolder: string,
+ verifySecret: string,
+ mongoOptions: {
+ username: string,
+ password: string,
+ database: string,
+ host: string,
+ },
+ baseUrl: string,
+ pastebinApiKey: string,
+ pastebinUsername: string,
+ pastebinPassword: string,
+ rapidApiKey: string
+};
+
+export default config;
\ No newline at end of file
diff --git a/src/config/main.json b/src/config/main.json
deleted file mode 100644
index 64abe93..0000000
--- a/src/config/main.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- developmentToken: "Your development bot token (Used for testing in one server, rather than production)",
- developmentGuildID: "Your development guild ID",
- enableDevelopment: true,
- token: "Your bot token",
- managementGuildID: "Your management guild ID (Used for running management commands on the bot)",
- owners: [],
- commandsFolder: "Your built commands folder (usually dist/commands)",
- eventsFolder: "Your built events folder (usually dist/events)",
- messageContextFolder: "Your built message context folder (usually dist/context/messages)",
- userContextFolder: "Your built user context folder (usually dist/context/users)",
- verifySecret:
- "If using verify, enter a code here which matches the secret sent back by your website. You can use a random code if you do not have one already. (Optional)",
- mongoUrl: "Your Mongo connection string, e.g. mongodb://127.0.0.1:27017",
- baseUrl: "Your website where buttons such as Verify and Role menu will link to, e.g. https://example.com/",
- pastebinApiKey: "An API key for pastebin (optional)",
- pastebinUsername: "Your pastebin username (optional)",
- pastebinPassword: "Your pastebin password (optional)",
- rapidApiKey: "Your RapidAPI key (optional), used for Unscan"
-}
diff --git a/src/context/messages/purgeto.ts b/src/context/messages/purgeto.ts
index df52e0b..aef159b 100644
--- a/src/context/messages/purgeto.ts
+++ b/src/context/messages/purgeto.ts
@@ -1,10 +1,9 @@
import confirmationMessage from '../../utils/confirmationMessage.js';
import EmojiEmbed from '../../utils/generateEmojiEmbed.js';
import { LoadingEmbed } from '../../utils/defaults.js';
-import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildTextBasedChannel, MessageContextMenuCommandInteraction } from "discord.js";
+import Discord, { ActionRowBuilder, ButtonBuilder, ButtonStyle, ContextMenuCommandBuilder, GuildMember, GuildTextBasedChannel, Message, MessageContextMenuCommandInteraction } from "discord.js";
import client from "../../utils/client.js";
-import getEmojiByName from '../../utils/getEmojiByName.js';
-import { JSONTranscriptFromMessageArray, JSONTranscriptToHumanReadable } from "../../utils/logTranscripts.js";
+import { messageException } from '../../utils/createTemporaryStorage.js';
const command = new ContextMenuCommandBuilder()
.setName("Purge up to here")
@@ -13,7 +12,7 @@
async function waitForButton(m: Discord.Message, member: Discord.GuildMember): Promise<boolean> {
let component;
try {
- component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id && i.channel!.id === m.channel.id });
+ component = m.awaitMessageComponent({ time: 200000, filter: (i) => i.user.id === member.id && i.channel!.id === m.channel.id && i.message.id === m.id });
} catch (e) {
return false;
}
@@ -171,7 +170,7 @@
calculateType: "messageDelete",
color: NucleusColors.red,
emoji: "PUNISH.BAN.RED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(interaction.user.id, `\`${interaction.user.id}\``),
@@ -184,13 +183,18 @@
}
};
log(data);
- const transcript = JSONTranscriptToHumanReadable(JSONTranscriptFromMessageArray(deleted.map((m) => m as Discord.Message))!);
- const attachmentObject = {
- attachment: Buffer.from(transcript),
- name: `purge-${channel.id}-${Date.now()}.txt`,
- description: "Purge log"
- };
- const m = (await interaction.editReply({
+ const messages: Message[] = deleted.map(m => m).filter(m => m instanceof Message).map(m => m as Message);
+ if (messages.length === 1) messageException(interaction.guild!.id, interaction.channel.id, messages[0]!.id)
+ 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 transcript = await client.database.transcripts.createTranscript(messageArray, interaction, interaction.member as GuildMember);
+ const code = await client.database.transcripts.create(transcript);
+ await interaction.editReply({
embeds: [
new EmojiEmbed()
.setEmoji("CHANNEL.PURGE.GREEN")
@@ -200,47 +204,10 @@
],
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 = async (_interaction: MessageContextMenuCommandInteraction) => {
diff --git a/src/context/users/userinfo.ts b/src/context/users/userinfo.ts
index 3b1a6bd..496f84e 100644
--- a/src/context/users/userinfo.ts
+++ b/src/context/users/userinfo.ts
@@ -5,6 +5,7 @@
.setName("User info")
const callback = async (interaction: UserContextMenuCommandInteraction) => {
+ console.log("callback")
const guild = interaction.guild!
let member = interaction.targetMember
if (!member) member = await guild.members.fetch(interaction.targetId)
@@ -12,6 +13,7 @@
}
const check = async (_interaction: UserContextMenuCommandInteraction) => {
+ console.log("check")
return true;
}
diff --git a/src/events/channelCreate.ts b/src/events/channelCreate.ts
index dda37af..b42ded7 100644
--- a/src/events/channelCreate.ts
+++ b/src/events/channelCreate.ts
@@ -4,7 +4,8 @@
export const event = "channelCreate";
export async function callback(client: NucleusClient, channel: GuildBasedChannel) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(channel.guild.id, "channelUpdate")) return;
const auditLog = (await getAuditLog(channel.guild, AuditLogEvent.ChannelCreate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as GuildBasedChannel)!.id === channel.id)[0];
if (!auditLog) return;
@@ -62,7 +63,7 @@
calculateType: "channelUpdate",
color: NucleusColors.green,
emoji: emoji,
- timestamp: channel.createdTimestamp
+ timestamp: channel.createdTimestamp ?? Date.now()
},
list: {
channelId: entry(channel.id, `\`${channel.id}\``),
diff --git a/src/events/channelDelete.ts b/src/events/channelDelete.ts
index 4780700..890a15f 100644
--- a/src/events/channelDelete.ts
+++ b/src/events/channelDelete.ts
@@ -14,8 +14,8 @@
export const event = "channelDelete";
export async function callback(client: NucleusClient, channel: GuildBasedChannel) {
- const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser } = client.logger;
- // const audit = auditLog.entries.filter((entry: GuildAuditLogsEntry) => entry.target!.id === channel.id).first();
+ const { getAuditLog, log, isLogging, NucleusColors, entry, renderDelta, renderUser } = client.logger;
+ if (!await isLogging(channel.guild.id, "channelUpdate")) return;
const auditLog = (await getAuditLog(channel.guild, AuditLogEvent.ChannelDelete))
.filter((entry: GuildAuditLogsEntry) => (entry.target as GuildBasedChannel)!.id === channel.id)[0];
if (!auditLog) return;
@@ -82,7 +82,7 @@
),
nsfw: null,
created: entry(channel.createdTimestamp, renderDelta(channel.createdTimestamp!)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ deleted: entry(Date.now(), renderDelta(Date.now())),
deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!))
};
if ((channel instanceof BaseGuildTextChannel || channel instanceof StageChannel) && channel.topic !== null)
diff --git a/src/events/channelUpdate.ts b/src/events/channelUpdate.ts
index 7c80e12..052acc1 100644
--- a/src/events/channelUpdate.ts
+++ b/src/events/channelUpdate.ts
@@ -32,11 +32,11 @@
topic?: ReturnType<typeof entry>;
bitrate?: ReturnType<typeof entry>;
userLimit?: ReturnType<typeof entry>;
- rateLimitPerUser?: ReturnType<typeof entry>;
parent?: ReturnType<typeof entry>;
permissionOverwrites?: ReturnType<typeof entry>;
region?: ReturnType<typeof entry>;
maxUsers?: ReturnType<typeof entry>;
+ autoArchiveDuration?: ReturnType<typeof entry>;
}
@@ -44,8 +44,9 @@
export const event = "channelUpdate";
export async function callback(client: NucleusClient, oldChannel: GuildChannel, newChannel: GuildChannel) {
+ const { getAuditLog, log, isLogging, NucleusColors, renderDelta, renderUser, renderChannel } = client.logger;
+ if (!await isLogging(newChannel.guild.id, "channelUpdate")) return;
const config = await client.memory.readGuildInfo(newChannel.guild.id);
- const { getAuditLog, log, NucleusColors, renderDelta, renderUser, renderChannel } = client.logger;
entry = client.logger.entry;
if (newChannel.parent && newChannel.parent.id === config.tickets.category) return;
@@ -60,7 +61,7 @@
const changes: channelChanges = {
channelId: entry(newChannel.id, `\`${newChannel.id}\``),
channel: entry(newChannel.id, renderChannel(newChannel)),
- edited: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ edited: entry(Date.now(), renderDelta(Date.now())),
editedBy: entry(auditLog.executor!.id, renderUser((await newChannel.guild.members.fetch(auditLog.executor!.id)).user)),
};
if (oldChannel.name !== newChannel.name) changes.name = entry([oldChannel.name, newChannel.name], `${oldChannel.name} -> ${newChannel.name}`);
@@ -68,12 +69,16 @@
changes.position = entry([oldChannel.position.toString(), newChannel.position.toString()], `${oldChannel.position} -> ${newChannel.position}`);
switch (newChannel.type) {
+ case ChannelType.PrivateThread:
+ case ChannelType.PublicThread: {
+ return;
+ }
case ChannelType.GuildText: {
emoji = "CHANNEL.TEXT.EDIT";
readableType = "Text";
displayName = "Text Channel";
- let oldTopic = (oldChannel as TextChannel).topic,
- newTopic = (newChannel as TextChannel).topic;
+ let oldTopic = (oldChannel as TextChannel).topic ?? "*None*",
+ newTopic = (oldChannel as TextChannel).topic ?? "*None*";
if (oldTopic) {
if (oldTopic.length > 256)
oldTopic = `\`\`\`\n${oldTopic.replace("`", "'").substring(0, 253) + "..."}\n\`\`\``;
@@ -91,14 +96,20 @@
const nsfw = ["", ""];
nsfw[0] = (oldChannel as TextChannel).nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`;
nsfw[1] = (newChannel as TextChannel).nsfw ? `${getEmojiByName("CONTROL.TICK")} Yes` : `${getEmojiByName("CONTROL.CROSS")} No`;
- if ((oldChannel as TextChannel).topic !== (newChannel as TextChannel).topic)
+ if (oldTopic !== newTopic)
changes.description = entry([(oldChannel as TextChannel).topic ?? "", (newChannel as TextChannel).topic ?? ""], `\nBefore: ${oldTopic}\nAfter: ${newTopic}`);
if ((oldChannel as TextChannel).nsfw !== (newChannel as TextChannel).nsfw) changes.nsfw = entry([(oldChannel as TextChannel).nsfw ? "On" : "Off", (newChannel as TextChannel).nsfw ? "On" : "Off"], `${nsfw[0]} -> ${nsfw[1]}`);
- if ((oldChannel as TextChannel).rateLimitPerUser !== (newChannel as TextChannel).rateLimitPerUser && (oldChannel as TextChannel).rateLimitPerUser !== 0)
- changes.rateLimitPerUser = entry(
+ if ((oldChannel as TextChannel).rateLimitPerUser !== (newChannel as TextChannel).rateLimitPerUser)
+ changes.slowmode = entry(
[((oldChannel as TextChannel).rateLimitPerUser).toString(), ((newChannel as TextChannel).rateLimitPerUser).toString()],
`${humanizeDuration((oldChannel as TextChannel).rateLimitPerUser * 1000)} -> ${humanizeDuration((newChannel as TextChannel).rateLimitPerUser * 1000)}`
);
+ if((oldChannel as TextChannel).defaultAutoArchiveDuration !== (newChannel as TextChannel).defaultAutoArchiveDuration) {
+ changes.autoArchiveDuration = entry(
+ [((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString(), ((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString()],
+ `${humanizeDuration(((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)} -> ${humanizeDuration(((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)}`
+ );
+ }
break;
}
@@ -122,8 +133,15 @@
} else {
newTopic = "None";
}
- if ((oldChannel as TextChannel).nsfw !== (newChannel as TextChannel).nsfw)
+ if ((oldChannel as TextChannel).nsfw !== (newChannel as TextChannel).nsfw) {
changes.nsfw = entry([(oldChannel as TextChannel).nsfw ? "On" : "Off", (newChannel as TextChannel).nsfw ? "On" : "Off"], `${(oldChannel as TextChannel).nsfw ? "On" : "Off"} -> ${(newChannel as TextChannel).nsfw ? "On" : "Off"}`);
+ }
+ if((oldChannel as TextChannel).defaultAutoArchiveDuration !== (newChannel as TextChannel).defaultAutoArchiveDuration) {
+ changes.autoArchiveDuration = entry(
+ [((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString(), ((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320).toString()],
+ `${humanizeDuration(((oldChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)} -> ${humanizeDuration(((newChannel as TextChannel).defaultAutoArchiveDuration ?? 4320) * 60 * 1000)}`
+ );
+ }
break;
}
case ChannelType.GuildVoice: {
@@ -174,7 +192,7 @@
if ((oldChannel as StageChannel).rtcRegion !== (newChannel as StageChannel).rtcRegion)
changes.region = entry(
[(oldChannel as StageChannel).rtcRegion ?? "Automatic", (newChannel as StageChannel).rtcRegion ?? "Automatic"],
- `${(oldChannel as StageChannel).rtcRegion?.toUpperCase() ?? "Automatic"} -> ${(newChannel as StageChannel).rtcRegion?.toUpperCase() ?? "Automatic"}`
+ `${capitalize((oldChannel as StageChannel).rtcRegion?.toLowerCase() ?? "automatic")} -> ${capitalize((newChannel as StageChannel).rtcRegion?.toLowerCase() ?? "automatic")}`
);
break;
}
diff --git a/src/events/emojiCreate.ts b/src/events/emojiCreate.ts
index 8023abc..2630295 100644
--- a/src/events/emojiCreate.ts
+++ b/src/events/emojiCreate.ts
@@ -4,9 +4,10 @@
export const event = "emojiCreate";
export async function callback(client: NucleusClient, emoji: GuildEmoji) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger;
+ const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger;
+ if (!await isLogging(emoji.guild.id, "emojiUpdate")) return;
const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiCreate))
- .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0];
+ .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0];
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
const data = {
diff --git a/src/events/emojiDelete.ts b/src/events/emojiDelete.ts
index f607cf4..c4b488e 100644
--- a/src/events/emojiDelete.ts
+++ b/src/events/emojiDelete.ts
@@ -4,8 +4,9 @@
export const event = "emojiDelete";
export async function callback(client: NucleusClient, emoji: GuildEmoji) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger;
- const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiCreate))
+ const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger;
+ if (!await isLogging(emoji.guild.id, "emojiUpdate")) return;
+ const auditLog = (await getAuditLog(emoji.guild, AuditLogEvent.EmojiDelete))
.filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === emoji.id)[0];
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
diff --git a/src/events/emojiUpdate.ts b/src/events/emojiUpdate.ts
index 201dd42..98ff558 100644
--- a/src/events/emojiUpdate.ts
+++ b/src/events/emojiUpdate.ts
@@ -4,9 +4,10 @@
export const event = "emojiUpdate";
export async function callback(client: NucleusClient, oldEmoji: GuildEmoji, newEmoji: GuildEmoji) {
- const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser, renderEmoji } = client.logger;
+ const { getAuditLog, log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderEmoji } = client.logger;
+ if (!(await isLogging(newEmoji.guild.id, "emojiUpdate"))) return;
- const auditLog = (await getAuditLog(newEmoji.guild, AuditLogEvent.EmojiCreate))
+ const auditLog = (await getAuditLog(newEmoji.guild, AuditLogEvent.EmojiUpdate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as GuildEmoji)!.id === newEmoji.id)[0];
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
diff --git a/src/events/guildBanAdd.ts b/src/events/guildBanAdd.ts
index 3d96245..cac4b41 100644
--- a/src/events/guildBanAdd.ts
+++ b/src/events/guildBanAdd.ts
@@ -7,10 +7,11 @@
export const event = "guildBanAdd";
export async function callback(client: NucleusClient, ban: GuildBan) {
+ const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger;
await statsChannelRemove(client, undefined, ban.guild, ban.user);
purgeByUser(ban.user.id, ban.guild.id);
- const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger;
- const auditLog: GuildAuditLogsEntry | undefined = (await getAuditLog(ban.guild, AuditLogEvent.EmojiCreate))
+ if (!(await isLogging(ban.guild.id, "guildMemberPunish"))) return;
+ const auditLog: GuildAuditLogsEntry | undefined = (await getAuditLog(ban.guild, AuditLogEvent.MemberBanAdd))
.filter((entry: GuildAuditLogsEntry) => ((entry.target! as User).id === ban.user.id))[0];
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
@@ -22,12 +23,12 @@
calculateType: "guildMemberPunish",
color: NucleusColors.red,
emoji: "PUNISH.BAN.RED",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(ban.user.id, `\`${ban.user.id}\``),
name: entry(ban.user.id, renderUser(ban.user)),
- banned: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ banned: entry(Date.now(), renderDelta(Date.now())),
bannedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
reason: entry(auditLog.reason, auditLog.reason ? `\n> ${auditLog.reason}` : "*No reason provided.*"),
accountCreated: entry(ban.user.createdTimestamp, renderDelta(ban.user.createdTimestamp)),
diff --git a/src/events/guildBanRemove.ts b/src/events/guildBanRemove.ts
index bcb70d5..3be4560 100644
--- a/src/events/guildBanRemove.ts
+++ b/src/events/guildBanRemove.ts
@@ -1,14 +1,13 @@
import type { GuildAuditLogsEntry, GuildBan, User } from "discord.js";
import { AuditLogEvent } from "discord.js";
-import { purgeByUser } from "../actions/tickets/delete.js";
import type { NucleusClient } from "../utils/client.js";
export const event = "guildBanRemove";
export async function callback(client: NucleusClient, ban: GuildBan) {
- purgeByUser(ban.user.id, ban.guild.id);
- const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger;
- const auditLog = (await getAuditLog(ban.guild, AuditLogEvent.EmojiCreate))
+ const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger;
+ if (!await isLogging(ban.guild.id, "guildMemberPunish")) return;
+ const auditLog = (await getAuditLog(ban.guild, AuditLogEvent.MemberBanRemove))
.filter((entry: GuildAuditLogsEntry) => ((entry.target! as User).id === ban.user.id))[0];
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
@@ -20,12 +19,12 @@
calculateType: "guildMemberPunish",
color: NucleusColors.green,
emoji: "PUNISH.BAN.GREEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(ban.user.id, `\`${ban.user.id}\``),
name: entry(ban.user.id, renderUser(ban.user)),
- unbanned: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ unbanned: entry(Date.now(), renderDelta(Date.now())),
unbannedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
accountCreated: entry(ban.user.createdTimestamp, renderDelta(ban.user.createdTimestamp))
},
diff --git a/src/events/guildMemberUpdate.ts b/src/events/guildMemberUpdate.ts
index 8889f57..721978f 100644
--- a/src/events/guildMemberUpdate.ts
+++ b/src/events/guildMemberUpdate.ts
@@ -1,11 +1,76 @@
import { AuditLogEvent, GuildAuditLogsEntry, GuildMember } from "discord.js";
import type { NucleusClient } from "../utils/client.js";
+import type { LoggerOptions } from "../utils/log.js";
+import { generalException } from "../utils/createTemporaryStorage.js";
export const event = "guildMemberUpdate";
export async function callback(client: NucleusClient, before: GuildMember, after: GuildMember) {
const { log, NucleusColors, entry, renderUser, renderDelta, getAuditLog } = client.logger;
- const auditLog = (await getAuditLog(after.guild, AuditLogEvent.EmojiCreate))
+ if(before.guild.id === "684492926528651336") {
+ await client.database.premium.checkAllPremium(after)
+ }
+
+ if(!before.roles.cache.equals(after.roles.cache)) {
+ const auditLog = (await getAuditLog(after.guild, AuditLogEvent.MemberRoleUpdate))
+ .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === after.id)[0];
+ if (!auditLog) return;
+ if (client.noLog.includes(`${after.guild.id}${after.id}${auditLog.id}`)) return;
+ generalException(`${after.guild.id}${after.id}${auditLog.id}`);
+ if (auditLog.executor!.id !== client.user!.id) {
+ const rolesAdded = after.roles.cache.filter(role => !before.roles.cache.has(role.id));
+ const rolesRemoved = before.roles.cache.filter(role => !after.roles.cache.has(role.id));
+ let displayName = "Roles Removed";
+ let color = NucleusColors.red;
+ let emoji = "GUILD.ROLES.DELETE";
+ if(rolesAdded.size > 0 && rolesRemoved.size > 0) {displayName = "Roles Changed"; color = NucleusColors.yellow; emoji = "GUILD.ROLES.EDIT";}
+ else if(rolesAdded.size > 0) {displayName = "Roles Added"; color = NucleusColors.green; emoji = "GUILD.ROLES.CREATE";}
+ const removedEntry = rolesRemoved.map(role => role.id);
+ const addedEntry = rolesAdded.map(role => role.id);
+
+ let list = {
+ memberId: entry(after.id, `\`${after.id}\``),
+ name: entry(after.user.id, renderUser(after.user)),
+ };
+
+ if (rolesAdded.size > 0) {
+ list = Object.assign(list, {rolesAdded: entry(addedEntry, addedEntry.map(id => `<@&${id}>`).join(", "))});
+ }
+ if (rolesRemoved.size > 0) {
+ list = Object.assign(list, {rolesRemoved: entry(removedEntry, removedEntry.map(id => `<@&${id}>`).join(", "))});
+ }
+
+ list = Object.assign(list, {
+ changed: entry(Date.now(), renderDelta(Date.now())),
+ changedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!))
+ });
+
+ let data: LoggerOptions = {
+ meta: {
+ type: "memberUpdate",
+ displayName: displayName,
+ calculateType: "guildMemberUpdate",
+ color: color,
+ emoji: emoji,
+ timestamp: Date.now()
+ },
+ list: {},
+ hidden: {
+ guild: after.guild.id
+ }
+ };
+
+ if(rolesAdded.size > 0) {
+ list = Object.assign(list, {rolesAdded: entry(addedEntry, addedEntry.map(id => `<@&${id}>`).join(", "))});
+ }
+ if(rolesRemoved.size > 0) {
+ list = Object.assign(list, {rolesRemoved: entry(removedEntry, removedEntry.map(id => `<@&${id}>`).join(", "))});
+ }
+ data = Object.assign(data, {list: list});
+ log(data);
+ }
+ }
+ const auditLog = (await getAuditLog(after.guild, AuditLogEvent.MemberUpdate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === after.id)[0];
if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
@@ -26,14 +91,14 @@
calculateType: "guildMemberUpdate",
color: NucleusColors.yellow,
emoji: "PUNISH.NICKNAME.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(after.id, `\`${after.id}\``),
name: entry(after.user.id, renderUser(after.user)),
before: entry(before.nickname, before.nickname ? before.nickname : "*None*"),
after: entry(after.nickname, after.nickname ? after.nickname : "*None*"),
- changed: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ changed: entry(Date.now(), renderDelta(Date.now())),
changedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!))
},
hidden: {
@@ -41,9 +106,10 @@
}
};
log(data);
- } else if (
- (before.communicationDisabledUntilTimestamp ?? 0) < new Date().getTime() &&
- (after.communicationDisabledUntil ?? 0) > new Date().getTime() // TODO: test this
+ }
+ if (
+ (before.communicationDisabledUntilTimestamp ?? 0) < Date.now() &&
+ new Date(after.communicationDisabledUntil ?? 0).getTime() > Date.now()
) {
await client.database.history.create(
"mute",
@@ -62,7 +128,7 @@
calculateType: "guildMemberPunish",
color: NucleusColors.yellow,
emoji: "PUNISH.MUTE.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(after.id, `\`${after.id}\``),
@@ -71,7 +137,7 @@
after.communicationDisabledUntilTimestamp,
renderDelta(after.communicationDisabledUntilTimestamp!)
),
- muted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ muted: entry(Date.now(), renderDelta(Date.now())),
mutedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
reason: entry(auditLog.reason, auditLog.reason ? auditLog.reason : "\n> *No reason provided*")
},
@@ -85,10 +151,11 @@
user: after.id,
expires: after.communicationDisabledUntilTimestamp
});
- } else if (
+ }
+ if (
after.communicationDisabledUntil === null &&
before.communicationDisabledUntilTimestamp !== null &&
- new Date().getTime() >= auditLog.createdTimestamp
+ Date.now() >= auditLog.createdTimestamp
) {
await client.database.history.create(
"unmute",
@@ -107,12 +174,12 @@
calculateType: "guildMemberPunish",
color: NucleusColors.green,
emoji: "PUNISH.MUTE.GREEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(after.id, `\`${after.id}\``),
name: entry(after.user.id, renderUser(after.user)),
- unmuted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ unmuted: entry(Date.now(), renderDelta(Date.now())),
unmutedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!))
},
hidden: {
diff --git a/src/events/guildUpdate.ts b/src/events/guildUpdate.ts
index 8690af2..6b25e48 100644
--- a/src/events/guildUpdate.ts
+++ b/src/events/guildUpdate.ts
@@ -6,7 +6,8 @@
export async function callback(client: NucleusClient, before: Guild, after: Guild) {
await statsChannelUpdate(client, after.members.me!);
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ if (!await isLogging(after.id, "guildUpdate")) return;
const auditLog = (await getAuditLog(after, AuditLogEvent.GuildUpdate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Guild)!.id === after.id)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -74,7 +75,7 @@
);
if (!Object.keys(list).length) return;
- list["updated"] = entry(new Date().getTime(), renderDelta(new Date().getTime()));
+ list["updated"] = entry(Date.now(), renderDelta(Date.now()));
list["updatedBy"] = entry(auditLog.executor!.id, renderUser(auditLog.executor!));
const data = {
meta: {
@@ -83,7 +84,7 @@
calculateType: "guildUpdate",
color: NucleusColors.yellow,
emoji: "GUILD.YELLOW",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: list,
hidden: {
diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts
index a22045b..80c2c1b 100644
--- a/src/events/interactionCreate.ts
+++ b/src/events/interactionCreate.ts
@@ -31,14 +31,14 @@
await message.fetch();
if (message.embeds.length === 0) return;
const embed = message.embeds[0];
- const newColour = accept ? "Success" : "Danger";
+ const newcolor = accept ? "Success" : "Danger";
const footer = {text: `Suggestion ${accept ? "accepted" : "denied"} by ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL()};
const newEmbed = new EmojiEmbed()
.setTitle(embed!.title!)
.setDescription(embed!.description!)
.setFooter(footer)
- .setStatus(newColour);
+ .setStatus(newcolor);
await interaction.update({embeds: [newEmbed], components: []});
}
diff --git a/src/events/inviteCreate.ts b/src/events/inviteCreate.ts
index a267f09..34f66dc 100644
--- a/src/events/inviteCreate.ts
+++ b/src/events/inviteCreate.ts
@@ -7,7 +7,8 @@
export async function callback(client: NucleusClient, invite: Invite) {
if(!invite.guild) return; // This is a DM invite (not a guild invite
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(invite.guild.id, "guildUpdate")) return;
const auditLog = (await getAuditLog(invite.guild as Guild, AuditLogEvent.InviteCreate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Invite)!.code === invite.code)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -18,7 +19,7 @@
calculateType: "guildUpdate",
color: NucleusColors.green,
emoji: "INVITE.CREATE",
- timestamp: invite.createdTimestamp
+ timestamp: invite.createdTimestamp ?? Date.now()
},
list: {
channel: entry(invite.channel!.id, renderChannel(invite.channel as GuildChannel)),
diff --git a/src/events/inviteDelete.ts b/src/events/inviteDelete.ts
index 1ded432..456af90 100644
--- a/src/events/inviteDelete.ts
+++ b/src/events/inviteDelete.ts
@@ -7,7 +7,8 @@
export async function callback(client: NucleusClient, invite: Invite) {
if(!invite.guild) return; // This is a DM invite (not a guild invite
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(invite.guild.id, "guildUpdate")) return;
const auditLog = (await getAuditLog(invite.guild as Guild, AuditLogEvent.InviteDelete))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Invite)!.code === invite.code)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -18,14 +19,14 @@
calculateType: "guildUpdate",
color: NucleusColors.red,
emoji: "INVITE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
channel: entry(invite.channel!.id, renderChannel(invite.channel as GuildChannel)),
link: entry(invite.url, invite.url),
expires: entry(invite.maxAge, invite.maxAge ? humanizeDuration(invite.maxAge * 1000) : "Never"),
deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime()))
+ deleted: entry(Date.now(), renderDelta(Date.now()))
},
hidden: {
guild: invite.guild!.id
diff --git a/src/events/memberJoin.ts b/src/events/memberJoin.ts
index daf195a..77b111f 100644
--- a/src/events/memberJoin.ts
+++ b/src/events/memberJoin.ts
@@ -8,7 +8,8 @@
export async function callback(client: NucleusClient, member: GuildMember) {
welcome(client, member);
statsChannelAdd(client, member);
- const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ const { log, isLogging, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ if (!await isLogging(member.guild.id, "guildMemberUpdate")) return;
await client.database.history.create("join", member.guild.id, member.user, null, null);
const data = {
meta: {
@@ -17,7 +18,7 @@
calculateType: "guildMemberUpdate",
color: NucleusColors.green,
emoji: "MEMBER" + (member.user.bot ? ".BOT" : "") + ".JOIN",
- timestamp: member.joinedTimestamp
+ timestamp: member.joinedTimestamp ?? Date.now()
},
list: {
memberId: entry(member.id, `\`${member.id}\``),
diff --git a/src/events/memberLeave.ts b/src/events/memberLeave.ts
index e70fb3c..8b3d1b1 100644
--- a/src/events/memberLeave.ts
+++ b/src/events/memberLeave.ts
@@ -7,22 +7,36 @@
export const event = "guildMemberRemove";
export async function callback(client: NucleusClient, member: GuildMember) {
+ const startTime = Date.now() - 10 * 1000;
purgeByUser(member.id, member.guild.id);
await statsChannelRemove(client, member);
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
- const auditLog = (await getAuditLog(member.guild as Guild, AuditLogEvent.MemberKick))
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ if (!await isLogging(member.guild.id, "guildMemberUpdate")) return;
+ const kickAuditLog = (await getAuditLog(member.guild as Guild, AuditLogEvent.MemberKick))
+ .filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === member.id)[0];
+ const banAuditLog = (await getAuditLog(member.guild as Guild, AuditLogEvent.MemberBanAdd))
.filter((entry: GuildAuditLogsEntry) => (entry.target as GuildMember)!.id === member.id)[0];
let type = "leave";
- if (auditLog) {
- if (auditLog.executor!.id === client.user!.id) return;
- if (auditLog.createdAt.valueOf() - 100 >= new Date().getTime()) {
+ if (kickAuditLog) {
+ if (kickAuditLog.executor!.id === client.user!.id) return;
+ if (kickAuditLog.createdAt.getTime() >= startTime) {
type = "kick";
}
}
+ if (banAuditLog) {
+ if (banAuditLog.executor!.id === client.user!.id) return;
+ if (banAuditLog.createdAt.getTime() >= startTime) {
+ if (!kickAuditLog) {
+ return
+ } else if (kickAuditLog.createdAt.valueOf() < banAuditLog.createdAt.valueOf()) {
+ return
+ }
+ }
+ }
let data;
if (type === "kick") {
- if (!auditLog) return;
- await client.database.history.create("kick", member.guild.id, member.user, auditLog.executor, auditLog.reason);
+ if (!kickAuditLog) return;
+ await client.database.history.create("kick", member.guild.id, member.user, kickAuditLog.executor, kickAuditLog.reason);
data = {
meta: {
type: "memberKick",
@@ -30,15 +44,15 @@
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: entry(member.joinedTimestamp, renderDelta(member.joinedTimestamp?.valueOf()!)),
- kicked: entry(new Date().getTime(), renderDelta(new Date().getTime())),
- kickedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
- reason: entry(auditLog.reason, auditLog.reason ? `\n> ${auditLog.reason}` : "*No reason provided.*"),
+ kicked: entry(Date.now(), renderDelta(Date.now())),
+ kickedBy: entry(kickAuditLog.executor!.id, renderUser(kickAuditLog.executor!)),
+ reason: entry(kickAuditLog.reason, kickAuditLog.reason ? `\n> ${kickAuditLog.reason}` : "*No reason provided.*"),
accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)),
serverMemberCount: member.guild.memberCount
},
@@ -55,13 +69,13 @@
calculateType: "guildMemberUpdate",
color: NucleusColors.red,
emoji: "MEMBER." + (member.user.bot ? "BOT." : "") + "LEAVE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(member.id, `\`${member.id}\``),
name: entry(member.id, renderUser(member.user)),
joined: entry(member.joinedTimestamp, renderDelta(member.joinedTimestamp?.valueOf()!)),
- left: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ left: entry(Date.now(), renderDelta(Date.now())),
accountCreated: entry(member.user.createdTimestamp, renderDelta(member.user.createdTimestamp)),
serverMemberCount: member.guild.memberCount
},
diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts
index 69bc542..4f525fc 100644
--- a/src/events/messageCreate.ts
+++ b/src/events/messageCreate.ts
@@ -19,12 +19,23 @@
console.log(e);
}
- const { log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
const fileNames = await logAttachment(message);
const content = message.content.toLowerCase() || "";
const config = await client.memory.readGuildInfo(message.guild.id);
+ if(config.filters.clean.channels.includes(message.channel.id)) {
+ const memberRoles = message.member!.roles.cache.map(role => role.id);
+ const roleAllow = config.filters.clean.allowed.roles.some(role => memberRoles.includes(role));
+ const userAllow = config.filters.clean.allowed.users.includes(message.author.id);
+ if(!roleAllow && !userAllow) return await message.delete();
+ }
+
+ if (config.autoPublish.enabled && config.autoPublish.channels.includes(message.channel.id)) {
+ await message.crosspost();
+ }
+
const filter = getEmojiByName("ICONS.FILTER");
let attachmentJump = "";
if (config.logging.attachments.saved[message.channel.id + message.id]) {
@@ -34,7 +45,7 @@
messageId: entry(message.id, `\`${message.id}\``),
sentBy: entry(message.author.id, renderUser(message.author)),
sentIn: entry(message.channel.id, renderChannel(message.channel)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ deleted: entry(Date.now(), renderDelta(Date.now())),
mentions: message.mentions.users.size,
attachments: entry(message.attachments.size, message.attachments.size + attachmentJump),
repliedTo: entry(
@@ -57,7 +68,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -78,7 +89,8 @@
if (fileNames.files.length > 0) {
for (const element of fileNames.files) {
const url = element.url ? element.url : element.local;
- if (/\.(jpg|jpeg|png|gif|gifv|webm|webp|mp4|wav|mp3|ogg)$/.test(url)) {
+ if (/\.(j(pe?g|fif)|a?png|gifv?|w(eb[mp]|av)|mp([34]|eg-\d)|ogg|avi|h\.26(4|5)|cda)$/.test(url.toLowerCase())) {
+ // jpg|jpeg|png|apng|gif|gifv|webm|webp|mp4|wav|mp3|ogg|jfif|MPEG-#|avi|h.264|h.265
if (
config.filters.images.NSFW &&
!(message.channel instanceof ThreadChannel ? message.channel.parent?.nsfw : message.channel.nsfw)
@@ -93,7 +105,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -128,7 +140,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -158,7 +170,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -189,7 +201,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -221,7 +233,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -253,7 +265,7 @@
calculateType: "autoModeratorDeleted",
color: NucleusColors.red,
emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start:
@@ -271,6 +283,7 @@
}
if (config.filters.pings.everyone && message.mentions.everyone) {
+ if(!await isLogging(message.guild.id, "messageMassPing")) return;
const data = {
meta: {
type: "everyonePing",
@@ -278,7 +291,7 @@
calculateType: "messageMassPing",
color: NucleusColors.yellow,
emoji: "MESSAGE.PING.EVERYONE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*"
@@ -295,6 +308,7 @@
if (!config.filters.pings.allowed.roles.includes(roleId)) {
messageException(message.guild.id, message.channel.id, message.id);
await message.delete();
+ if(!await isLogging(message.guild.id, "messageMassPing")) return;
const data = {
meta: {
type: "rolePing",
@@ -302,7 +316,7 @@
calculateType: "messageMassPing",
color: NucleusColors.yellow,
emoji: "MESSAGE.PING.ROLE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start: content
@@ -321,6 +335,7 @@
if (message.mentions.users.size >= config.filters.pings.mass && config.filters.pings.mass) {
messageException(message.guild.id, message.channel.id, message.id);
await message.delete();
+ if(!await isLogging(message.guild.id, "messageMassPing")) return;
const data = {
meta: {
type: "massPing",
@@ -328,7 +343,7 @@
calculateType: "messageMassPing",
color: NucleusColors.yellow,
emoji: "MESSAGE.PING.MASS",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
separate: {
start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*"
diff --git a/src/events/messageDelete.ts b/src/events/messageDelete.ts
index f8433fc..aac83f4 100644
--- a/src/events/messageDelete.ts
+++ b/src/events/messageDelete.ts
@@ -4,61 +4,59 @@
export const event = "messageDelete";
export async function callback(client: NucleusClient, message: Message) {
- try {
- if (message.author.id === client.user!.id) return;
- if (client.noLog.includes(`${message.id}/${message.channel.id}/${message.id}`)) return;
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
- const auditLog = (await getAuditLog(message.guild!, AuditLogEvent.MemberBanAdd))
- .filter((entry: GuildAuditLogsEntry) => (entry.target! as User).id === message.author.id)[0];
- if (auditLog) {
- if (auditLog.createdTimestamp - 1000 < new Date().getTime()) return;
- }
- const replyTo = message.reference;
- let content = message.cleanContent;
- content.replace("`", "\\`");
- if (content.length > 256) content = content.substring(0, 253) + "...";
- const attachments =
- message.attachments.size + (
- message.content.match(
- /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi
- ) ?? []
- ).length;
- let attachmentJump = "";
- const config = (await client.database.guilds.read(message.guild!.id)).logging.attachments.saved[
- message.channel.id + message.id
- ];
- if (config) { attachmentJump = ` [[View attachments]](${config})`; }
- const data = {
- meta: {
- type: "messageDelete",
- displayName: "Message Deleted",
- calculateType: "messageDelete",
- color: NucleusColors.red,
- emoji: "MESSAGE.DELETE",
- timestamp: new Date().getTime()
- },
- separate: {
- start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*"
- },
- list: {
- messageId: entry(message.id, `\`${message.id}\``),
- sentBy: entry(message.author.id, renderUser(message.author)),
- sentIn: entry(message.channel.id, renderChannel(message.channel as Discord.GuildChannel | Discord.ThreadChannel)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
- mentions: message.mentions.users.size,
- attachments: entry(attachments, attachments + attachmentJump),
- repliedTo: entry(
- replyTo ? replyTo.messageId! : null,
- replyTo ? `[[Jump to message]](https://discord.com/channels/${message.guild!.id}/${message.channel.id}/${replyTo.messageId})`
- : "None"
- )
- },
- hidden: {
- guild: message.guild!.id
- }
- };
- log(data);
- } catch (e) {
- console.log(e);
+ if (message.author.id === client.user!.id) return;
+ if (message.author.bot) return;
+ if (client.noLog.includes(`${message.guild!.id}/${message.channel.id}/${message.id}`)) return;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(message.guild!.id, "messageDelete")) return;
+ const auditLog = (await getAuditLog(message.guild!, AuditLogEvent.MemberBanAdd))
+ .filter((entry: GuildAuditLogsEntry) => (entry.target! as User).id === message.author.id)[0];
+ if (auditLog) {
+ if (auditLog.createdTimestamp - 1000 < Date.now()) return;
}
+ const replyTo = message.reference;
+ let content = message.cleanContent;
+ content.replace("`", "\\`");
+ if (content.length > 256) content = content.substring(0, 253) + "...";
+ const attachments =
+ message.attachments.size + (
+ message.content.match(
+ /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/gi
+ ) ?? []
+ ).length;
+ let attachmentJump = "";
+ const config = (await client.database.guilds.read(message.guild!.id)).logging.attachments.saved[
+ message.channel.id + message.id
+ ];
+ if (config) { attachmentJump = ` [[View attachments]](${config})`; }
+ const data = {
+ meta: {
+ type: "messageDelete",
+ displayName: "Message Deleted",
+ calculateType: "messageDelete",
+ color: NucleusColors.red,
+ emoji: "MESSAGE.DELETE",
+ timestamp: Date.now()
+ },
+ separate: {
+ start: content ? `**Message:**\n\`\`\`${content}\`\`\`` : "**Message:** *Message had no content*"
+ },
+ list: {
+ messageId: entry(message.id, `\`${message.id}\``),
+ sentBy: entry(message.author.id, renderUser(message.author)),
+ sentIn: entry(message.channel.id, renderChannel(message.channel as Discord.GuildChannel | Discord.ThreadChannel)),
+ deleted: entry(Date.now(), renderDelta(Date.now())),
+ mentions: message.mentions.users.size,
+ attachments: entry(attachments, attachments + attachmentJump),
+ repliedTo: entry(
+ replyTo ? replyTo.messageId! : null,
+ replyTo ? `[[Jump to message]](https://discord.com/channels/${message.guild!.id}/${message.channel.id}/${replyTo.messageId})`
+ : "None"
+ )
+ },
+ hidden: {
+ guild: message.guild!.id
+ }
+ };
+ log(data);
}
diff --git a/src/events/messageEdit.ts b/src/events/messageEdit.ts
index 20624fe..f5a28a4 100644
--- a/src/events/messageEdit.ts
+++ b/src/events/messageEdit.ts
@@ -6,8 +6,10 @@
export async function callback(client: NucleusClient, oldMessage: Message, newMessage: Message) {
if (newMessage.author.id === client.user!.id) return;
+ if (newMessage.author.bot) return;
if (!newMessage.guild) return;
- const { log, NucleusColors, entry, renderUser, renderDelta, renderNumberDelta, renderChannel } = client.logger;
+ const { log, isLogging, NucleusColors, entry, renderUser, renderDelta, renderNumberDelta, renderChannel } = client.logger;
+
const replyTo: MessageReference | null = newMessage.reference;
let newContent = newMessage.cleanContent.replaceAll("`", "‘");
let oldContent = oldMessage.cleanContent.replaceAll("`", "‘");
@@ -18,7 +20,8 @@
if (config) {
attachmentJump = ` [[View attachments]](${config})`;
}
- if (newContent === oldContent && newMessage.attachments.size === oldMessage.attachments.size) {
+ if (newMessage.crosspostable !== oldMessage.crosspostable) {
+ if(!await isLogging(newMessage.guild.id, "messageAnnounce")) return;
if (!replyTo) {
const data = {
meta: {
@@ -27,7 +30,7 @@
calculateType: "messageAnnounce",
color: NucleusColors.yellow,
emoji: "MESSAGE.CREATE",
- timestamp: newMessage.editedTimestamp
+ timestamp: newMessage.editedTimestamp ?? Date.now()
},
separate: {
end: `[[Jump to message]](${newMessage.url})`
@@ -57,6 +60,7 @@
return log(data);
}
}
+ if (!await isLogging(newMessage.guild.id, "messageUpdate")) return;
if (!newMessage.editedTimestamp) {
return;
}
diff --git a/src/events/roleCreate.ts b/src/events/roleCreate.ts
index d253ce7..be385f0 100644
--- a/src/events/roleCreate.ts
+++ b/src/events/roleCreate.ts
@@ -4,7 +4,8 @@
export const event = "roleCreate";
export async function callback(client: NucleusClient, role: Role) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderRole } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderRole } = client.logger;
+ if (!await isLogging(role.guild.id, "guildRoleUpdate")) return;
if (role.managed) return;
const auditLog = (await getAuditLog(role.guild as Guild, AuditLogEvent.RoleCreate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === role.id)[0]!;
diff --git a/src/events/roleDelete.ts b/src/events/roleDelete.ts
index f41241b..b207f4f 100644
--- a/src/events/roleDelete.ts
+++ b/src/events/roleDelete.ts
@@ -5,7 +5,8 @@
export const event = "roleDelete";
export async function callback(client: NucleusClient, role: Role) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ if (!await isLogging(role.guild.id, "guildRoleUpdate")) return;
if (role.managed) return;
const auditLog = (await getAuditLog(role.guild as Guild, AuditLogEvent.RoleDelete))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === role.id)[0]!;
@@ -34,7 +35,7 @@
members: entry(role.members.size, `${role.members.size}`),
deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
created: entry(role.createdTimestamp, renderDelta(role.createdTimestamp)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime()))
+ deleted: entry(Date.now(), renderDelta(Date.now()))
},
hidden: {
guild: role.guild.id
diff --git a/src/events/roleUpdate.ts b/src/events/roleUpdate.ts
index 924ec3e..8d9ef10 100644
--- a/src/events/roleUpdate.ts
+++ b/src/events/roleUpdate.ts
@@ -5,16 +5,17 @@
export const event = "roleUpdate";
export async function callback(client: NucleusClient, oldRole: Role, newRole: Role) {
- const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser, renderRole } = client.logger;
-
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderDelta, renderUser, renderRole } = client.logger;
+ if (!await isLogging(newRole.guild.id, "guildRoleUpdate")) return;
const auditLog = (await getAuditLog(newRole.guild as Guild, AuditLogEvent.RoleUpdate))
- .filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === newRole.id)[0]!;
+ .filter((entry: GuildAuditLogsEntry) => (entry.target as Role)!.id === newRole.id)[0];
+ if (!auditLog) return;
if (auditLog.executor!.id === client.user!.id) return;
const changes: Record<string, ReturnType<typeof entry>> = {
roleId: entry(newRole.id, `\`${newRole.id}\``),
role: entry(newRole.id, renderRole(newRole)),
- edited: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ edited: entry(Date.now(), renderDelta(Date.now())),
editedBy: entry(auditLog.executor!.id, renderUser((await newRole.guild.members.fetch(auditLog.executor!.id)).user))
};
const mentionable = ["", ""];
@@ -31,6 +32,12 @@
changes["mentionable"] = entry([oldRole.mentionable, newRole.mentionable], `${mentionable[0]} -> ${mentionable[1]}`);
if (oldRole.hexColor !== newRole.hexColor)
changes["color"] = entry([oldRole.hexColor, newRole.hexColor], `\`${oldRole.hexColor}\` -> \`${newRole.hexColor}\``);
+ if (oldRole.permissions.bitfield !== newRole.permissions.bitfield) {
+ changes["permissions"] = entry(
+ [oldRole.permissions.bitfield.toString(), newRole.permissions.bitfield.toString()],
+ `[[Old]](https://discordapi.com/permissions.html#${oldRole.permissions.bitfield.toString()}) -> [[New]](https://discordapi.com/permissions.html#${newRole.permissions.bitfield.toString()})`
+ );
+ }
if (Object.keys(changes).length === 4) return;
@@ -47,6 +54,6 @@
hidden: {
guild: newRole.guild.id
}
- }; // TODO: show perms changed (webpage)
+ }; // TODO: make our own page for this
log(data);
}
diff --git a/src/events/stickerCreate.ts b/src/events/stickerCreate.ts
index b341ae9..5d2e443 100644
--- a/src/events/stickerCreate.ts
+++ b/src/events/stickerCreate.ts
@@ -1,13 +1,17 @@
import type { NucleusClient } from "../utils/client.js";
import { AuditLogEvent, GuildAuditLogsEntry, Sticker } from "discord.js";
+import { generalException } from "../utils/createTemporaryStorage.js";
-export const event = "stickerDelete";
+export const event = "stickerCreate";
export async function callback(client: NucleusClient, sticker: Sticker) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
- const auditLog = (await getAuditLog(sticker.guild!, AuditLogEvent.EmojiCreate))
- .filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === sticker.id)[0]!;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ if (!await isLogging(sticker.guild!.id, "stickerUpdate")) return;
+ const auditLog = (await getAuditLog(sticker.guild!, AuditLogEvent.StickerCreate))
+ .filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === sticker.id)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
+ if (client.noLog.includes(`${sticker.guild!.id}${auditLog.id}`)) return;
+ generalException(`${sticker.guild!.id}${auditLog.id}`);
const data = {
meta: {
type: "stickerCreate",
diff --git a/src/events/stickerDelete.ts b/src/events/stickerDelete.ts
index ce26a85..d123f44 100644
--- a/src/events/stickerDelete.ts
+++ b/src/events/stickerDelete.ts
@@ -4,7 +4,8 @@
export const event = "stickerDelete";
export async function callback(client: NucleusClient, sticker: Sticker) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
+ if (!await isLogging(sticker.guild!.id, "stickerUpdate")) return;
const auditLog = (await getAuditLog(sticker.guild!, AuditLogEvent.StickerDelete))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === sticker.id)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -14,7 +15,7 @@
displayName: "Sticker Deleted",
calculateType: "stickerUpdate",
color: NucleusColors.red,
- sticker: "GUILD.sticker.DELETE",
+ emoji: "GUILD.EMOJI.DELETE",
timestamp: auditLog.createdTimestamp
},
list: {
diff --git a/src/events/stickerUpdate.ts b/src/events/stickerUpdate.ts
index ed01b71..aef28a4 100644
--- a/src/events/stickerUpdate.ts
+++ b/src/events/stickerUpdate.ts
@@ -4,8 +4,8 @@
export const event = "stickerUpdate";
export async function callback(client: NucleusClient, oldSticker: Sticker, newSticker: Sticker) {
- const { getAuditLog, log, NucleusColors, entry, renderDelta, renderUser } = client.logger;
-
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderDelta, renderUser } = client.logger;
+ if (!await isLogging(newSticker.guild!.id, "stickerUpdate")) return;
if (oldSticker.name === newSticker.name) return;
const auditLog = (await getAuditLog(newSticker.guild!, AuditLogEvent.StickerUpdate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as Sticker)!.id === newSticker.id)[0]!;
diff --git a/src/events/threadCreate.ts b/src/events/threadCreate.ts
index 6d3225c..f56e1bb 100644
--- a/src/events/threadCreate.ts
+++ b/src/events/threadCreate.ts
@@ -5,7 +5,8 @@
export const event = "threadCreate";
export async function callback(client: NucleusClient, thread: ThreadChannel) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(thread.guild.id, "channelUpdate")) return;
const auditLog = (await getAuditLog(thread.guild, AuditLogEvent.ThreadCreate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as ThreadChannel)!.id === thread.id)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -22,7 +23,7 @@
calculateType: "channelUpdate",
color: NucleusColors.green,
emoji: "CHANNEL.TEXT.CREATE",
- timestamp: thread.createdTimestamp
+ timestamp: thread.createdTimestamp ?? Date.now()
},
list: {
threadId: entry(thread.id, `\`${thread.id}\``),
diff --git a/src/events/threadDelete.ts b/src/events/threadDelete.ts
index 429f63a..bfac75e 100644
--- a/src/events/threadDelete.ts
+++ b/src/events/threadDelete.ts
@@ -5,7 +5,8 @@
export const event = "threadDelete";
export async function callback(client: NucleusClient, thread: ThreadChannel) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(thread.guild.id, "channelUpdate")) return;
const auditLog = (await getAuditLog(thread.guild, AuditLogEvent.ThreadDelete))
.filter((entry: GuildAuditLogsEntry) => (entry.target as ThreadChannel)!.id === thread.id)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -22,7 +23,7 @@
calculateType: "channelUpdate",
color: NucleusColors.red,
emoji: "CHANNEL.TEXT.DELETE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
threadId: entry(thread.id, `\`${thread.id}\``),
@@ -38,7 +39,7 @@
membersInThread: entry(thread.memberCount, thread.memberCount!.toString()),
deletedBy: entry(auditLog.executor!.id, renderUser(auditLog.executor!)),
created: entry(thread.createdTimestamp, renderDelta(thread.createdTimestamp!)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime()))
+ deleted: entry(Date.now(), renderDelta(Date.now()))
},
hidden: {
guild: thread.guild.id
diff --git a/src/events/threadUpdate.ts b/src/events/threadUpdate.ts
index 555b17f..af792bc 100644
--- a/src/events/threadUpdate.ts
+++ b/src/events/threadUpdate.ts
@@ -6,7 +6,8 @@
export const event = "threadUpdate";
export async function callback(client: NucleusClient, oldThread: ThreadChannel, newThread: ThreadChannel) {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderDelta, renderChannel } = client.logger;
+ if (!await isLogging(newThread.guild.id, "channelUpdate")) return;
const auditLog = (await getAuditLog(newThread.guild, AuditLogEvent.ThreadUpdate))
.filter((entry: GuildAuditLogsEntry) => (entry.target as ThreadChannel)!.id === newThread.id)[0]!;
if (auditLog.executor!.id === client.user!.id) return;
@@ -37,7 +38,7 @@
);
}
if (!(Object.keys(list).length - 3)) return;
- list["updated"] = entry(new Date().getTime(), renderDelta(new Date().getTime()));
+ list["updated"] = entry(Date.now(), renderDelta(Date.now()));
list["updatedBy"] = entry(auditLog.executor!.id, renderUser(auditLog.executor!));
const data = {
meta: {
@@ -46,7 +47,7 @@
calculateType: "channelUpdate",
color: NucleusColors.yellow,
emoji: "CHANNEL.TEXT.EDIT",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: list,
hidden: {
diff --git a/src/events/webhookUpdate.ts b/src/events/webhookUpdate.ts
index e5f07dd..004f754 100644
--- a/src/events/webhookUpdate.ts
+++ b/src/events/webhookUpdate.ts
@@ -10,18 +10,18 @@
export async function callback(client: NucleusClient, channel: Discord.GuildChannel) {
try {
- const { getAuditLog, log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger;
+ const { getAuditLog, isLogging, log, NucleusColors, entry, renderUser, renderChannel, renderDelta } = client.logger;
+ if (!await isLogging(channel.guild.id, "webhookUpdate")) return;
const auditCreate = (await getAuditLog(channel.guild, AuditLogEvent.WebhookCreate))
- .filter((entry: GuildAuditLogsEntry) => (entry.target as Webhook)!.id === channel.id)[0]!;
- const auditDelete = (await getAuditLog(channel.guild, AuditLogEvent.WebhookDelete))
- .filter((entry: GuildAuditLogsEntry) => (entry.target as Webhook)!.id === channel.id)[0];
- const auditUpdate = (await getAuditLog(channel.guild, AuditLogEvent.WebhookUpdate))
- .filter((entry: GuildAuditLogsEntry) => (entry.target as Webhook)!.id === channel.id)[0];
-
- if (!auditUpdate && !auditDelete) return;
+ .filter((entry: GuildAuditLogsEntry | null) => (entry?.target) ? (entry.target as Webhook)!.channelId === channel.id : false)[0];
+ const auditDelete = (await getAuditLog(channel.guild, AuditLogEvent.WebhookDelete, 0))
+ .filter((entry: GuildAuditLogsEntry | null) => (entry?.target) ? (entry.target as Webhook)!.channelId === channel.id : false)[0];
+ const auditUpdate = (await getAuditLog(channel.guild, AuditLogEvent.WebhookUpdate, 0))
+ .filter((entry: GuildAuditLogsEntry | null) => (entry?.target) ? (entry.target as Webhook)!.channelId === channel.id : false)[0];
+ if (!auditCreate && !auditUpdate && !auditDelete) return;
let action: "Create" | "Update" | "Delete" = "Create";
let list: Record<string, ReturnType<typeof entry> | string> = {};
- const createTimestamp = auditCreate.createdTimestamp;
+ const createTimestamp = auditCreate ? auditCreate.createdTimestamp : 0;
const deleteTimestamp = auditDelete ? auditDelete.createdTimestamp : 0;
const updateTimestamp = auditUpdate ? auditUpdate.createdTimestamp : 0;
if (updateTimestamp > createTimestamp && updateTimestamp > deleteTimestamp && auditUpdate) {
@@ -46,7 +46,7 @@
(auditUpdate.target! as Extract<GuildAuditLogsEntry, {createdTimestamp: number}>).createdTimestamp,
renderDelta((auditUpdate.target! as Extract<GuildAuditLogsEntry, {createdTimestamp: number}>).createdTimestamp)
);
- list["edited"] = entry(after["editedTimestamp"]!, renderDelta(new Date().getTime()));
+ list["edited"] = entry(after["editedTimestamp"]!, renderDelta(Date.now()));
list["editedBy"] = entry(auditUpdate.executor!.id, renderUser(auditUpdate.executor!));
action = "Update";
} else if (deleteTimestamp > createTimestamp && deleteTimestamp > updateTimestamp && auditDelete) {
@@ -61,7 +61,7 @@
name: entry(before["name"]!, `${before["name"]}`),
channel: entry(before["channel_id"]!, renderChannel((await client.channels.fetch(before["channel_id"]!)) as GuildChannel)),
created: entry((auditDelete.target! as Extract<GuildAuditLogsEntry, {createdTimestamp: number}>).createdTimestamp, renderDelta((auditDelete.target! as Extract<GuildAuditLogsEntry, {createdTimestamp: number}>).createdTimestamp)),
- deleted: entry(new Date().getTime(), renderDelta(new Date().getTime())),
+ deleted: entry(Date.now(), renderDelta(Date.now())),
deletedBy: entry(
auditDelete.executor!.id,
renderUser((await channel.guild.members.fetch(auditDelete.executor!.id)).user)
@@ -80,10 +80,10 @@
name: entry(before["name"]!, `${before["name"]}`),
channel: entry(before["channel_id"]!, renderChannel(await client.channels.fetch(before["channel_id"]!) as GuildChannel)),
createdBy: entry(
- auditCreate.executor!.id,
- renderUser((await channel.guild.members.fetch(auditCreate.executor!.id)).user)
+ auditCreate!.executor!.id,
+ renderUser((await channel.guild.members.fetch(auditCreate!.executor!.id)).user)
),
- created: entry(new Date().getTime(), renderDelta(new Date().getTime()))
+ created: entry(Date.now(), renderDelta(Date.now()))
};
}
const cols = {
@@ -98,7 +98,7 @@
calculateType: "webhookUpdate",
color: NucleusColors[cols[action] as keyof typeof NucleusColors],
emoji: "WEBHOOK." + action.toUpperCase(),
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: list,
hidden: {
diff --git a/src/index.ts b/src/index.ts
index 362b805..12f6659 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,17 +1,24 @@
import runServer from "./api/index.js";
import client from "./utils/client.js";
-import config from "./config/main.json" assert { type: "json" };
+import config from "./config/main.js";
import register from "./utils/commandRegistration/register.js";
import { record as recordPerformance } from "./utils/performanceTesting/record.js";
-client.on("ready", () => {
+client.on("ready", async () => {
console.log(`Logged in as ${client.user!.tag}!`);
register();
runServer(client);
+ if (config.enableDevelopment) {
+ client.fetchedCommands = await client.guilds.cache.get(config.developmentGuildID)?.commands.fetch()!;
+ } else {
+ client.fetchedCommands = await client.application?.commands.fetch()!;
+ }
+ await client.database.premium.checkAllPremium();
});
+
process.on("unhandledRejection", (err) => { console.error(err) });
process.on("uncaughtException", (err) => { console.error(err) });
await client.login(config.enableDevelopment ? config.developmentToken : config.token)
-await recordPerformance();
\ No newline at end of file
+await recordPerformance();
diff --git a/src/premium/attachmentLogs.ts b/src/premium/attachmentLogs.ts
index 156503a..b2c8391 100644
--- a/src/premium/attachmentLogs.ts
+++ b/src/premium/attachmentLogs.ts
@@ -1,4 +1,4 @@
-import { getCommandMentionByName } from './../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from './../utils/getCommandDataByName.js';
import client from "../utils/client.js";
import keyValueList from "../utils/generateKeyValueList.js";
import singleNotify from "../utils/singleNotify.js";
@@ -8,12 +8,13 @@
import type { GuildTextBasedChannel, Message } from "discord.js";
export default async function logAttachment(message: Message): Promise<AttachmentLogSchema> {
+ if (message.guild) client.database.premium.hasPremium(message.guild.id).finally(() => {});
if (!message.guild) throw new Error("Tried to log an attachment in a non-guild message");
const { renderUser, renderChannel, renderDelta } = client.logger;
const attachments = [];
for (const attachment of message.attachments.values()) {
attachments.push({
- local: await saveAttachment(attachment.url),
+ local: (await saveAttachment(attachment.url))[0],
url: attachment.url,
height: attachment.height,
width: attachment.width,
@@ -24,7 +25,7 @@
for (const link of links) {
if (link.toLowerCase().match(/\.(jpg|jpeg|png|gif|gifv|webm|webp|mp4|wav|mp3|ogg)$/gi)) {
attachments.push({
- local: await saveAttachment(link),
+ local: (await saveAttachment(link))[0],
url: link,
height: null,
width: null
@@ -38,17 +39,17 @@
singleNotify(
"noAttachmentLogChannel",
message.guild.id,
- `No channel set for attachment logging. You can set one with ${await getCommandMentionByName("settings/logs/attachments")}`,
+ `No channel set for attachment logging. You can set one with ${getCommandMentionByName("settings/logs/attachments")}`,
"Warning"
);
return { files: attachments };
}
- const channelObj = await client.channels.fetch(channel);
+ const channelObj = await message.guild.channels.fetch(channel);
if (!channelObj) {
singleNotify(
"attachmentLogChannelDeleted",
message.guild.id,
- `Your attachment history channel was deleted or is not longer accessible. You can set a new one with ${await getCommandMentionByName("settings/logs/attachments")}`,
+ `Your attachment history channel was deleted or is not longer accessible. You can set a new one with ${getCommandMentionByName("settings/logs/attachments")}`,
"Warning"
);
return { files: attachments };
@@ -70,7 +71,6 @@
],
files: attachments.map((file) => file.local)
});
- // await client.database.guilds.write(interaction.guild.id, {[`tags.${name}`]: value});
client.database.guilds.write(message.guild.id, {
[`logging.attachments.saved.${message.channel.id}${message.id}`]: m.url
});
diff --git a/src/premium/createTranscript.ts b/src/premium/createTranscript.ts
index 04fdc08..67aed04 100644
--- a/src/premium/createTranscript.ts
+++ b/src/premium/createTranscript.ts
@@ -7,18 +7,23 @@
MessageComponentInteraction,
TextChannel,
ButtonStyle,
- User
+ User,
+ ThreadChannel
} from "discord.js";
import EmojiEmbed from "../utils/generateEmojiEmbed.js";
import getEmojiByName from "../utils/getEmojiByName.js";
-import { PasteClient, Publicity, ExpireDate } from "pastebin-api";
import client from "../utils/client.js";
+import { messageException } from '../utils/createTemporaryStorage.js';
-const pbClient = new PasteClient(client.config.pastebinApiKey);
+const noTopic = new EmojiEmbed()
+ .setTitle("User not found")
+ .setDescription("There is no user associated with this ticket.")
+ .setStatus("Danger")
+ .setEmoji("CONTROL.BLOCKCROSS")
export default async function (interaction: CommandInteraction | MessageComponentInteraction) {
if (interaction.channel === null) return;
- if (!(interaction.channel instanceof TextChannel)) return;
+ if (!(interaction.channel instanceof TextChannel || interaction.channel instanceof ThreadChannel)) return;
const { log, NucleusColors, entry, renderUser, renderDelta } = client.logger;
let messages: Message[] = [];
@@ -29,95 +34,75 @@
const deleted = await (interaction.channel as TextChannel).bulkDelete(fetched, true);
deletedCount = deleted.size;
messages = messages.concat(Array.from(deleted.values() as Iterable<Message>));
+ if (messages.length === 1) messageException(interaction.guild!.id, interaction.channel.id, messages[0]!.id)
} while (deletedCount === 100);
+ messages = messages.filter(message => !(
+ message.components.some(
+ component => component.components.some(
+ child => child.customId?.includes("transcript") ?? false
+ )
+ )
+ ));
- let out = "";
- messages.reverse().forEach((message) => {
- if (!message.author.bot) {
- const sentDate = new Date(message.createdTimestamp);
- out += `${message.author.username}#${message.author.discriminator} (${
- message.author.id
- }) [${sentDate.toUTCString()}]\n`;
- const lines = message.content.split("\n");
- lines.forEach((line) => {
- out += `> ${line}\n`;
- });
- out += "\n\n";
- }
- });
- const topic = interaction.channel.topic;
- let member: GuildMember | null = null;
- if (topic !== null) {
- const part = topic.split(" ")[0] ?? null;
- if (part !== null) member = interaction.guild!.members.cache.get(part) ?? null;
- }
- let m: Message;
- if (out !== "") {
- const url = await pbClient.createPaste({
- code: out,
- expireDate: ExpireDate.Never,
- name:
- `Ticket Transcript ${
- member ? "for " + member.user.username + "#" + member.user.discriminator + " " : ""
- }` + `(Created at ${new Date(interaction.channel.createdTimestamp).toDateString()})`,
- publicity: Publicity.Unlisted
- });
- const guildConfig = await client.database.guilds.read(interaction.guild!.id);
- m = (await interaction.reply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Transcript")
- .setDescription(
- "You can view the transcript using the link below. You can save the link for later" +
- (guildConfig.logging.logs.channel
- ? ` or find it in <#${guildConfig.logging.logs.channel}> once you press delete below. After this the channel will be deleted.`
- : ".")
- )
- .setStatus("Success")
- .setEmoji("CONTROL.DOWNLOAD")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(url),
- new ButtonBuilder()
- .setLabel("Delete")
- .setStyle(ButtonStyle.Danger)
- .setCustomId("close")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- ])
- ],
- fetchReply: true
- })) as Message;
+ let topic
+ let member: GuildMember = interaction.guild?.members.me!;
+ if (interaction.channel instanceof TextChannel) {
+ topic = interaction.channel.topic;
+ if (topic === null) return await interaction.reply({ embeds: [noTopic] });
+ const mem = interaction.guild!.members.cache.get(topic.split(" ")[1]!);
+ if (mem) member = mem;
} else {
- m = (await interaction.reply({
- embeds: [
- new EmojiEmbed()
- .setTitle("Transcript")
- .setDescription(
- "The transcript was empty, so no changes were made. To delete this ticket, press the delete button below."
- )
- .setStatus("Success")
- .setEmoji("CONTROL.DOWNLOAD")
- ],
- components: [
- new ActionRowBuilder<ButtonBuilder>().addComponents([
- new ButtonBuilder()
- .setLabel("Delete")
- .setStyle(ButtonStyle.Danger)
- .setCustomId("close")
- .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
- ])
- ],
- fetchReply: true
- })) as Message;
+ topic = interaction.channel.name;
+ const split = topic.split("-").map(p => p.trim()) as [string, string, string];
+ const mem = interaction.guild!.members.cache.get(split[1])
+ if (mem) member = mem;
}
+
+ const newOut = await client.database.transcripts.createTranscript(messages, interaction, member);
+
+ const code = await client.database.transcripts.create(newOut);
+ if(!code) return await interaction.reply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Error")
+ .setDescription("An error occurred while creating the transcript.")
+ .setStatus("Danger")
+ .setEmoji("CONTROL.BLOCKCROSS")
+ ]
+ })
+ const guildConfig = await client.database.guilds.read(interaction.guild!.id);
+ const m: Message = (await interaction.reply({
+ embeds: [
+ new EmojiEmbed()
+ .setTitle("Transcript")
+ .setDescription(
+ "You can view the transcript using the link below. You can save the link for later" +
+ (guildConfig.logging.logs.channel
+ ? ` or find it in <#${guildConfig.logging.logs.channel}> once you press delete below. After this the channel will be deleted.`
+ : ".")
+ )
+ .setStatus("Success")
+ .setEmoji("CONTROL.DOWNLOAD")
+ ],
+ components: [
+ new ActionRowBuilder<ButtonBuilder>().addComponents([
+ new ButtonBuilder().setLabel("View").setStyle(ButtonStyle.Link).setURL(`https://clicks.codes/nucleus/transcript/${code}`),
+ new ButtonBuilder()
+ .setLabel("Delete")
+ .setStyle(ButtonStyle.Danger)
+ .setCustomId("close")
+ .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
+ ])
+ ],
+ fetchReply: true
+ })) as Message;
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 }
});
- i.deferUpdate();
+ await i.deferUpdate();
} catch {
return;
}
@@ -128,12 +113,13 @@
calculateType: "ticketUpdate",
color: NucleusColors.red,
emoji: "GUILD.TICKET.CLOSE",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
- ticketFor: member ? entry(member.id, renderUser(member.user)) : entry(null, "*Unknown*"),
+ ticketFor: entry(member.id, renderUser(member.user)),
deletedBy: entry(interaction.member!.user.id, renderUser(interaction.member!.user as User)),
- deleted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime()))
+ deleted: entry(Date.now().toString(), renderDelta(Date.now())),
+ transcript: entry(code, `https://clicks.codes/nucleus/transcript/${code}`)
},
hidden: {
guild: interaction.guild!.id
diff --git a/src/reflex/guide.ts b/src/reflex/guide.ts
index 6829ef2..a3027e4 100644
--- a/src/reflex/guide.ts
+++ b/src/reflex/guide.ts
@@ -1,9 +1,9 @@
+import { getCommandMentionByName } from './../utils/getCommandDataByName.js';
import { LoadingEmbed } from "../utils/defaults.js";
import Discord, {
ActionRowBuilder,
ButtonBuilder,
MessageComponentInteraction,
- StringSelectMenuInteraction,
Guild,
CommandInteraction,
GuildTextBasedChannel,
@@ -19,32 +19,43 @@
export default async (guild: Guild, interaction?: CommandInteraction) => {
let c: GuildTextBasedChannel | null = guild.publicUpdatesChannel ? guild.publicUpdatesChannel : guild.systemChannel;
c = c
- ? c
- : (guild.channels.cache.find(
- (ch) =>
- [
- ChannelType.GuildText,
- ChannelType.GuildAnnouncement,
- ChannelType.PublicThread,
- ChannelType.PrivateThread,
- ChannelType.AnnouncementThread
- ].includes(ch.type) &&
- ch.permissionsFor(guild.roles.everyone).has("SendMessages") &&
- ch.permissionsFor(guild.members.me!).has("EmbedLinks")
- ) as GuildTextBasedChannel | undefined) ?? null;
+ ? c
+ : (guild.channels.cache.find(
+ (ch) =>
+ [
+ ChannelType.GuildText,
+ ChannelType.GuildAnnouncement,
+ ChannelType.PublicThread,
+ ChannelType.PrivateThread,
+ ChannelType.AnnouncementThread
+ ].includes(ch.type) &&
+ ch.permissionsFor(guild.roles.everyone).has("SendMessages") &&
+ ch.permissionsFor(guild.members.me!).has("EmbedLinks")
+ ) as GuildTextBasedChannel | undefined) ?? null;
if (interaction) c = interaction.channel as GuildTextBasedChannel;
if (!c) {
return;
}
+ let m: Message;
+ if (interaction) {
+ m = (await interaction.reply({
+ embeds: LoadingEmbed,
+ fetchReply: true,
+ ephemeral: true
+ })) as Message;
+ } else {
+ m = await c.send({ embeds: LoadingEmbed });
+ }
+ let page = 0;
const pages = [
new Embed()
.setEmbed(
new EmojiEmbed()
.setTitle("Welcome to Nucleus")
.setDescription(
- "Thanks for adding Nucleus to your server\n\n" +
- "On the next few pages you can find instructions on getting started, and commands you may want to set up\n\n" +
- "If you need support, have questions or want features, you can let us know in [Clicks](https://discord.gg/bPaNnxe)"
+ "Thanks for adding Nucleus to your server!\n\n" +
+ "The next few pages will show what features Nucleus has to offer, and how to enable them.\n\n" +
+ "If you need support, have questions or want features, you can let us know in [Clicks](https://discord.gg/bPaNnxe)!"
)
.setEmoji("NUCLEUS.LOGO")
.setStatus("Danger")
@@ -55,15 +66,17 @@
new Embed()
.setEmbed(
new EmojiEmbed()
- .setTitle("Logging")
+ .setTitle("Logs")
.setDescription(
"Nucleus can log server events and keep you informed with what content is being posted to your server.\n" +
"We have 2 different types of logs, which each can be configured to send to a channel of your choice:\n" +
- "**General Logs:** These are events like kicks and channel changes etc.\n" +
- "**Warning Logs:** Warnings like NSFW avatars and spam etc. that may require action by a server staff member. " +
- "These go to to a separate staff notifications channel.\n\n" +
- "A general log channel can be set with `/settings log`\n" +
- "A warning log channel can be set with `/settings warnings channel`"
+ "**General:** These are events like kicks and channel changes etc.\n" +
+ `> These are standard logs and can be set with ${getCommandMentionByName("settings/logs/general")}\n` +
+ "**Warnings:** Warnings like NSFW avatars and spam etc. that may require action by a server staff member.\n" +
+ `> These may require special action by a moderator. You can set the channel with ${getCommandMentionByName("settings/logs/warnings")}\n` +
+ "**Attachments:** All images sent in the server - Used to keep a record of deleted images\n" +
+ `> Sent to a separate log channel to avoid spam. This can be set with ${getCommandMentionByName("settings/logs/attachments")}\n` +
+ `> ${getEmojiByName("NUCLEUS.PREMIUM")} Please note this feature is only available with ${getCommandMentionByName("nucleus/premium")}`
)
.setEmoji("ICONS.LOGGING")
.setStatus("Danger")
@@ -77,27 +90,15 @@
.setTitle("Moderation")
.setDescription(
"Nucleus has a number of commands that can be used to moderate your server.\n" +
- "These commands are all found under `/mod`, and they include:\n" +
- `**${getEmojiByName(
- "PUNISH.WARN.YELLOW"
- )} Warn:** The user is warned (via DM) that they violated server rules.\n` +
- `**${getEmojiByName(
- "PUNISH.CLEARHISTORY"
- )} Clear:** Some messages from a user are deleted in a channel.\n` +
- `**${getEmojiByName(
- "PUNISH.MUTE.YELLOW"
- )} Mute:** The user is unable to send messages or join voice chats.\n` +
- `**${getEmojiByName(
- "PUNISH.MUTE.GREEN"
- )} Unmute:** The user is able to send messages in the server.\n` +
- `**${getEmojiByName("PUNISH.KICK.RED")} Kick:** The user is removed from the server.\n` +
- `**${getEmojiByName(
- "PUNISH.SOFTBAN"
- )} Softban:** Kicks the user, deleting their messages from every channel.\n` +
- `**${getEmojiByName(
- "PUNISH.BAN.RED"
- )} Ban:** The user is removed from the server, and they are unable to rejoin.\n` +
- `**${getEmojiByName("PUNISH.BAN.GREEN")} Unban:** The user is able to rejoin the server.`
+ `These commands are all found under ${getCommandMentionByName(("mod"))}, and they include:\n` +
+ `${getEmojiByName("PUNISH.WARN.YELLOW")} ${getCommandMentionByName("mod/warn")}: The user is warned (via DM) that they violated server rules. More options given if DMs are disabled.\n` +
+ `${getEmojiByName("PUNISH.CLEARHISTORY")} ${getCommandMentionByName("mod/purge")}: Deletes messages in a channel, giving options to only delete messages by a certain user.\n` +
+ `${getEmojiByName("PUNISH.MUTE.YELLOW")} ${getCommandMentionByName("mod/mute")}: Stops users sending messages or joining voice chats.\n` +
+ `${getEmojiByName("PUNISH.MUTE.GREEN")} ${getCommandMentionByName("mod/unmute")}: Allows user to send messages and join voice chats.\n` +
+ `${getEmojiByName("PUNISH.KICK.RED")} ${getCommandMentionByName("mod/kick")}: Removes a member from the server. They will be able to rejoin.\n` +
+ `${getEmojiByName("PUNISH.SOFTBAN")} ${getCommandMentionByName("mod/softban")}: Kicks the user, deleting their messages from every channel in a given time frame.\n` +
+ `${getEmojiByName("PUNISH.BAN.RED")} ${getCommandMentionByName("mod/ban")}: Removes the user from the server, deleting messages from every channel and stops them from rejoining.\n` +
+ `${getEmojiByName("PUNISH.BAN.GREEN")} ${getCommandMentionByName("mod/unban")}: Allows a member to rejoin the server after being banned.`
)
.setEmoji("PUNISH.BAN.RED")
.setStatus("Danger")
@@ -111,9 +112,9 @@
.setTitle("Verify")
.setDescription(
"Nucleus has a verification system that allows users to prove they aren't bots.\n" +
- "This is done by running `/verify` which sends a message only the user can see, giving them a link to a CAPTCHA to verify.\n" +
- "After the user complete's the CAPTCHA, they are given a role and can use the permissions accordingly.\n" +
- "You can set the role given with `/settings verify`"
+ `This is done by running ${getCommandMentionByName("verify")} which sends a message only the user can see, giving them a link to a website to verify.\n` +
+ "After the user complete's the check, they are given a role, which can be set to unlock specific channels.\n" +
+ `You can set the role given with ${getCommandMentionByName("settings/verify")}`
)
.setEmoji("CONTROL.REDTICK")
.setStatus("Danger")
@@ -127,8 +128,8 @@
.setTitle("Content Scanning")
.setDescription(
"Nucleus has a content scanning system that automatically scans links and images sent by users.\n" +
- "Nucleus can detect, delete, and punish users for sending NSFW content, or links to scam or adult sites.\n" +
- "You can set the threshold for this in `/settings automation`" // TODO
+ "The staff team can be notified when an NSFW image is detected, or malicious links are sent.\n" +
+ `You can check and manage what to moderate in ${getCommandMentionByName("settings/automod")}`
)
.setEmoji("MOD.IMAGES.TOOSMALL")
.setStatus("Danger")
@@ -141,10 +142,12 @@
new EmojiEmbed()
.setTitle("Tickets")
.setDescription(
- "Nucleus has a ticket system that allows users to create tickets and have a support team respond to them.\n" +
- "Tickets can be created with `/ticket create` and a channel is created, pinging the user and support role.\n" +
- "When the ticket is resolved, anyone can run `/ticket close` (or click the button) to close it.\n" +
- "Running `/ticket close` again will delete the ticket."
+ "Nucleus has a ticket system which allows users to create tickets and talk to the server staff or support team.\n" +
+ `Tickets can be created by users with ${getCommandMentionByName("ticket/create")}, or by clicking a button created by moderators.\n` +
+ `After being created, a new channel or thread is created, and the user and support team are pinged. \n` +
+ `The category or channel to create threads in can be set with ${getCommandMentionByName("settings/tickets")}\n` +
+ `When the ticket is resolved, anyone can run ${getCommandMentionByName("ticket/close")} (or click the button) to close it.\n` +
+ `Running ${getCommandMentionByName("ticket/close")} again will delete the ticket.`
)
.setEmoji("GUILD.TICKET.CLOSE")
.setStatus("Danger")
@@ -157,11 +160,10 @@
new EmojiEmbed()
.setTitle("Tags")
.setDescription(
- "Add a tag system to your server with the `/tag` and `/tags` commands.\n" +
- "To create a tag, type `/tags create <tag name> <tag content>`.\n" +
- "Tag names and content can be edited with `/tags edit`.\n" +
- "To delete a tag, type `/tags delete <tag name>`.\n" +
- "To view all tags, type `/tags list`.\n"
+ "Nucleus allows you to create tags, which allow a message to be sent when a specific tag is typed.\n" +
+ `Tags can be created with ${getCommandMentionByName("tags/create")}, and can be edited with ${getCommandMentionByName("tags/edit")}\n` +
+ `Tags can be deleted with ${getCommandMentionByName("tags/delete")}, and can be listed with ${getCommandMentionByName("tags/list")}\n` +
+ `To use a tag, you can type ${getCommandMentionByName("tag")}, followed by the tag to send`
)
.setEmoji("PUNISH.NICKNAME.RED")
.setStatus("Danger")
@@ -174,29 +176,18 @@
new EmojiEmbed()
.setTitle("Premium")
.setDescription(
- "In the near future, we will be releasing extra premium only features.\n" +
- "These features will include:\n\n" +
- "**Attachment logs**\n> When a message with attachments is edited or deleted, the logs will also include the images sent.\n" +
- "\nPremium is not yet available. Check `/nucleus premium` for updates on features and pricing"
+ "Nucleus Premium allows you to use extra features in your server, which are useful but not essential.\n" +
+ "**No currently free commands will become premium features.**\n" +
+ "Premium features include creating ticket transcripts and attachment logs.\n\n" +
+ "Premium can be purchased in [our server](https://discord.gg/bPaNnxe) in the subscriptions page" // TODO: add a table graphic
)
- .setEmoji("NUCLEUS.COMMANDS.LOCK")
+ .setEmoji("NUCLEUS.PREMIUM")
.setStatus("Danger")
)
.setTitle("Premium")
.setDescription("Premium features")
.setPageId(7)
];
- let m: Message;
- if (interaction) {
- m = (await interaction.reply({
- embeds: LoadingEmbed,
- fetchReply: true,
- ephemeral: true
- })) as Message;
- } else {
- m = await c.send({ embeds: LoadingEmbed });
- }
- let page = 0;
const publicFilter = async (component: MessageComponentInteraction) => {
return (component.member as Discord.GuildMember).permissions.has("ManageGuild");
@@ -274,7 +265,7 @@
timedOut = true;
continue;
}
- i.deferUpdate();
+ await i.deferUpdate();
if (!("customId" in i.component)) {
continue;
} else if (i.component.customId === "left") {
@@ -285,8 +276,8 @@
selectPaneOpen = false;
} else if (i.component.customId === "select") {
selectPaneOpen = !selectPaneOpen;
- } else if (i.component.customId === "page") {
- page = parseInt((i as StringSelectMenuInteraction).values[0]!);
+ } else if (i.isStringSelectMenu() && i.component.customId === "page") {
+ page = parseInt(i.values[0]!);
selectPaneOpen = false;
} else {
cancelled = true;
diff --git a/src/reflex/scanners.ts b/src/reflex/scanners.ts
index 9761e4b..cf713e6 100644
--- a/src/reflex/scanners.ts
+++ b/src/reflex/scanners.ts
@@ -1,23 +1,27 @@
import fetch from "node-fetch";
-import FormData from "form-data";
-import { writeFileSync, createReadStream } from "fs";
+import fs, { writeFileSync, createReadStream } from "fs";
import generateFileName from "../utils/temp/generateFileName.js";
import Tesseract from "node-tesseract-ocr";
import type Discord from "discord.js";
import client from "../utils/client.js";
+import { createHash } from "crypto";
interface NSFWSchema {
nsfw: boolean;
+ errored?: boolean;
}
interface MalwareSchema {
safe: boolean;
+ errored?: boolean;
}
export async function testNSFW(link: string): Promise<NSFWSchema> {
- const p = await saveAttachment(link);
- const data = new FormData();
- console.log(link);
- data.append("file", createReadStream(p));
+ const [p, hash] = await saveAttachment(link);
+ const alreadyHaveCheck = await client.database.scanCache.read(hash)
+ if(alreadyHaveCheck) return { nsfw: alreadyHaveCheck.data };
+ const data = new URLSearchParams();
+ const r = createReadStream(p)
+ data.append("file", r.read(fs.statSync(p).size));
const result = await fetch("https://unscan.p.rapidapi.com/", {
method: "POST",
headers: {
@@ -26,20 +30,24 @@
},
body: data
})
- .then((response) => response.json() as Promise<NSFWSchema>)
+ .then((response) => response.status === 200 ? response.json() as Promise<NSFWSchema> : { nsfw: false, errored: true })
.catch((err) => {
console.error(err);
- return { nsfw: false };
+ return { nsfw: false, errored: true };
});
- console.log(result);
+ if(!result.errored) {
+ client.database.scanCache.write(hash, result.nsfw);
+ }
return { nsfw: result.nsfw };
}
export async function testMalware(link: string): Promise<MalwareSchema> {
- const p = await saveAttachment(link);
- const data = new FormData();
- data.append("file", createReadStream(p));
- console.log(link);
+ const [p, hash] = await saveAttachment(link);
+ const alreadyHaveCheck = await client.database.scanCache.read(hash)
+ if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data };
+ const data = new URLSearchParams();
+ const f = createReadStream(p);
+ data.append("file", f.read(fs.statSync(p).size));
const result = await fetch("https://unscan.p.rapidapi.com/malware", {
method: "POST",
headers: {
@@ -48,18 +56,21 @@
},
body: data
})
- .then((response) => response.json() as Promise<MalwareSchema>)
+ .then((response) => response.status === 200 ? response.json() as Promise<MalwareSchema> : { safe: true, errored: true })
.catch((err) => {
console.error(err);
- return { safe: true };
+ return { safe: true, errored: true };
});
- console.log(result);
+ if (!result.errored) {
+ client.database.scanCache.write(hash, result.safe);
+ }
return { safe: result.safe };
}
export async function testLink(link: string): Promise<{ safe: boolean; tags: string[] }> {
- console.log(link);
- const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/malware", {
+ const alreadyHaveCheck = await client.database.scanCache.read(link)
+ if(alreadyHaveCheck) return { safe: alreadyHaveCheck.data, tags: [] };
+ const scanned: { safe?: boolean; tags?: string[] } = await fetch("https://unscan.p.rapidapi.com/link", {
method: "POST",
headers: {
"X-RapidAPI-Key": client.config.rapidApiKey,
@@ -72,18 +83,19 @@
console.error(err);
return { safe: true, tags: [] };
});
- console.log(scanned);
+ client.database.scanCache.write(link, scanned.safe ?? true, []);
return {
safe: scanned.safe ?? true,
tags: scanned.tags ?? []
};
}
-export async function saveAttachment(link: string): Promise<string> {
- const image = (await (await fetch(link)).buffer()).toString("base64");
+export async function saveAttachment(link: string): Promise<[string, string]> {
+ const image = await (await fetch(link)).arrayBuffer()
const fileName = generateFileName(link.split("/").pop()!.split(".").pop()!);
- writeFileSync(fileName, image, "base64");
- return fileName;
+ const enc = new TextDecoder("utf-8");
+ writeFileSync(fileName, new DataView(image), "base64");
+ return [fileName, createHash('sha512').update(enc.decode(image), 'base64').digest('base64')];
}
const linkTypes = {
@@ -139,8 +151,7 @@
export async function NSFWCheck(element: string): Promise<boolean> {
try {
- const test = await testNSFW(element);
- return test.nsfw;
+ return (await testNSFW(element)).nsfw;
} catch {
return false;
}
diff --git a/src/reflex/statsChannelUpdate.ts b/src/reflex/statsChannelUpdate.ts
index db705d9..daa82fd 100644
--- a/src/reflex/statsChannelUpdate.ts
+++ b/src/reflex/statsChannelUpdate.ts
@@ -1,4 +1,4 @@
-import { getCommandMentionByName } from '../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from '../utils/getCommandDataByName.js';
import type { Guild, User } from "discord.js";
import type { NucleusClient } from "../utils/client.js";
import type { GuildMember } from "discord.js";
@@ -32,7 +32,7 @@
return singleNotify(
"statsChannelDeleted",
guild!.id,
- `One or more of your stats channels have been deleted. You can use ${await getCommandMentionByName("settings/stats")}.\n` +
+ `One or more of your stats channels have been deleted. You can use ${getCommandMentionByName("settings/stats")}.\n` +
`The channels name was: ${deleted!.name}`,
"Critical"
);
diff --git a/src/reflex/verify.ts b/src/reflex/verify.ts
index 573da5e..290e372 100644
--- a/src/reflex/verify.ts
+++ b/src/reflex/verify.ts
@@ -13,6 +13,8 @@
import { TestString, NSFWCheck } from "./scanners.js";
import createPageIndicator from "../utils/createPageIndicator.js";
import client from "../utils/client.js";
+import singleNotify from "../utils/singleNotify.js";
+import { getCommandMentionByName } from "../utils/getCommandDataByName.js";
export interface VerifySchema {
uID: string;
@@ -182,14 +184,14 @@
let itt = 0;
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
do {
- itt += 1;
+ itt ++;
code = "";
for (let i = 0; i < length; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
if (itt > 1000) {
itt = 0;
- length += 1;
+ length ++;
}
} while (code in verify);
const role: Role | null = await interaction.guild!.roles.fetch(config.verify.role);
@@ -206,7 +208,8 @@
.setEmoji("CONTROL.BLOCKCROSS")
]
});
- return; // TODO: SEN
+ singleNotify("verifyRoleDeleted", interaction.guild!.id, `The role given when a member is verified has been deleted. Use ${getCommandMentionByName("settings/verify")} to set a new one`, "Critical")
+ return;
}
verify[code] = {
uID: interaction.member!.user.id,
diff --git a/src/reflex/welcome.ts b/src/reflex/welcome.ts
index 87bb81a..c2eede3 100644
--- a/src/reflex/welcome.ts
+++ b/src/reflex/welcome.ts
@@ -1,4 +1,4 @@
-import { getCommandMentionByName } from './../utils/getCommandMentionByName.js';
+import { getCommandMentionByName } from './../utils/getCommandDataByName.js';
import type { NucleusClient } from "../utils/client.js";
import convertCurlyBracketString from "../utils/convertCurlyBracketString.js";
import client from "../utils/client.js";
@@ -27,19 +27,19 @@
});
} else {
const channel: GuildChannel | null = await member.guild.channels.fetch(config.welcome.channel) as GuildChannel | null;
- if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`, "Warning")
+ if (!channel) return await singleNotify("welcomeChannelDeleted", member.guild.id, `The welcome channel has been deleted or is no longer accessible. Use ${getCommandMentionByName("settings/welcome")} to set a new one`, "Warning")
if (!(channel instanceof BaseGuildTextChannel)) return;
if (channel.guild.id !== member.guild.id) return;
try {
await channel.send({
embeds: [new EmojiEmbed().setDescription(string).setStatus("Success")],
- content: (config.welcome.ping ? `<@${config.welcome.ping}>` : "") + `<@${member.id}>`
+ content: (config.welcome.ping ? `<@&${config.welcome.ping}>` : "") + `<@${member.id}>`
});
} catch (err) {
singleNotify(
"welcomeChannelDeleted",
member.guild.id,
- `The welcome channel has been deleted or is no longer accessible. Use ${await getCommandMentionByName("settings/welcome")} to set a new one`,
+ `The welcome channel has been deleted or is no longer accessible. Use ${getCommandMentionByName("settings/welcome")} to set a new one`,
"Warning"
)
}
diff --git a/src/utils/calculate.ts b/src/utils/calculate.ts
index 0bd5a9f..fde1340 100644
--- a/src/utils/calculate.ts
+++ b/src/utils/calculate.ts
@@ -17,16 +17,14 @@
"webhookUpdate",
"guildMemberVerify",
"autoModeratorDeleted",
- "nucleusSettingsUpdated",
- "ticketUpdate"
+ "ticketUpdate",
+ // "nucleusSettingsUpdated"
];
const tickets = ["support", "report", "question", "issue", "suggestion", "other"];
const toHexInteger = (permissions: string[], array?: string[]): string => {
- if (!array) {
- array = logs;
- }
+ if (!array) { array = logs; }
let int = 0n;
for (const perm of permissions) {
diff --git a/src/utils/client.ts b/src/utils/client.ts
index 46d9f92..b1fa31f 100644
--- a/src/utils/client.ts
+++ b/src/utils/client.ts
@@ -1,11 +1,11 @@
-import Discord, { Client, Interaction, AutocompleteInteraction, GatewayIntentBits } from 'discord.js';
+import Discord, { Client, Interaction, AutocompleteInteraction, Collection } from 'discord.js';
import { Logger } from "../utils/log.js";
import Memory from "../utils/memory.js";
import type { VerifySchema } from "../reflex/verify.js";
-import { Guilds, History, ModNotes, Premium, PerformanceTest } from "../utils/database.js";
+import { Guilds, History, ModNotes, Premium, PerformanceTest, ScanCache, Transcript, } from "../utils/database.js";
import EventScheduler from "../utils/eventScheduler.js";
import type { RoleMenuSchema } from "../actions/roleMenu.js";
-import config from "../config/main.json" assert { type: "json" };
+import config from "../config/main.js";
class NucleusClient extends Client {
@@ -22,36 +22,33 @@
premium: Premium;
eventScheduler: EventScheduler;
performanceTest: PerformanceTest;
+ scanCache: ScanCache;
+ transcripts: Transcript
};
preloadPage: Record<string, {command: string, argument: string}> = {}; // e.g. { channelID: { command: privacy, page: 3}}
- commands: Record<string, {
+ commands: Record<string, [{
command: Discord.SlashCommandBuilder |
((builder: Discord.SlashCommandBuilder) => Discord.SlashCommandBuilder) |
Discord.SlashCommandSubcommandBuilder | ((builder: Discord.SlashCommandSubcommandBuilder) => Discord.SlashCommandSubcommandBuilder) | Discord.SlashCommandSubcommandGroupBuilder | ((builder: Discord.SlashCommandSubcommandGroupBuilder) => Discord.SlashCommandSubcommandGroupBuilder),
callback: (interaction: Interaction) => Promise<void>,
- check: (interaction: Interaction) => Promise<boolean> | boolean,
+ check: (interaction: Interaction, partial: boolean) => Promise<boolean> | boolean,
autocomplete: (interaction: AutocompleteInteraction) => Promise<string[]>
- }> = {};
-
+ } | undefined, {name: string, description: string}]> = {};
+ fetchedCommands = new Collection<string, Discord.ApplicationCommand>();
constructor(database: typeof NucleusClient.prototype.database) {
- super({ intents: [
- GatewayIntentBits.Guilds,
- GatewayIntentBits.GuildMessages,
- GatewayIntentBits.MessageContent,
- GatewayIntentBits.GuildPresences,
- GatewayIntentBits.GuildMembers
- ]});
+ super({ intents: 0b1100011011011111111111});
this.database = database;
}
}
-
const client = new NucleusClient({
- guilds: await new Guilds().setup(),
+ guilds: await new Guilds(),
history: new History(),
notes: new ModNotes(),
premium: new Premium(),
eventScheduler: new EventScheduler(),
- performanceTest: new PerformanceTest()
+ performanceTest: new PerformanceTest(),
+ scanCache: new ScanCache(),
+ transcripts: new Transcript()
});
export default client;
diff --git a/src/utils/commandRegistration/register.ts b/src/utils/commandRegistration/register.ts
index 281be18..33c88b0 100644
--- a/src/utils/commandRegistration/register.ts
+++ b/src/utils/commandRegistration/register.ts
@@ -1,12 +1,12 @@
import type { CommandInteraction } from 'discord.js';
import Discord, { Interaction, SlashCommandBuilder, ApplicationCommandType } from 'discord.js';
-import config from "../../config/main.json" assert { type: "json" };
+import config from "../../config/main.js";
import client from "../client.js";
import fs from "fs";
import EmojiEmbed from '../generateEmojiEmbed.js';
import getEmojiByName from '../getEmojiByName.js';
-const colours = {
+const colors = {
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
@@ -26,23 +26,26 @@
for (const file of files) {
const last = i === files.length - 1 ? "└" : "├";
if (file.isDirectory()) {
- console.log(`${last}─ ${colours.yellow}Loading subcommands of ${file.name}${colours.none}`)
- const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`)).command;
- commands.push(fetched);
+ console.log(`${last}─ ${colors.yellow}Loading subcommands of ${file.name}${colors.none}`)
+ const fetched = (await import(`../../../${config.commandsFolder}/${file.name}/_meta.js`));
+ commands.push(fetched.command);
} else if (file.name.endsWith(".js")) {
- console.log(`${last}─ ${colours.yellow}Loading command ${file.name}${colours.none}`)
+ console.log(`${last}─ ${colors.yellow}Loading command ${file.name}${colors.none}`)
const fetched = (await import(`../../../${config.commandsFolder}/${file.name}`));
fetched.command.setDMPermission(fetched.allowedInDMs ?? false)
fetched.command.setNameLocalizations(fetched.nameLocalizations ?? {})
fetched.command.setDescriptionLocalizations(fetched.descriptionLocalizations ?? {})
- if (fetched.nameLocalizations || fetched.descriptionLocalizations) console.log("AAAAA")
+ // if (fetched.nameLocalizations || fetched.descriptionLocalizations)
commands.push(fetched.command);
- client.commands["commands/" + fetched.command.name] = fetched;
+ client.commands["commands/" + fetched.command.name] = [
+ fetched,
+ {name: fetched.name ?? fetched.command.name, description: fetched.description ?? fetched.command.description}
+ ];
}
i++;
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`)
}
- console.log(`${colours.yellow}Loaded ${commands.length} commands, processing...`)
+ console.log(`${colors.yellow}Loaded ${commands.length} commands, processing...`)
const processed = []
for (const subcommand of commands) {
@@ -53,7 +56,7 @@
}
}
- console.log(`${colours.green}Processed ${processed.length} commands`)
+ console.log(`${colors.green}Processed ${processed.length} commands${colors.none}`)
return processed;
};
@@ -70,15 +73,15 @@
const last = i === files.length - 1 ? "└" : "├";
i++;
try {
- console.log(`${last}─ ${colours.yellow}Loading event ${file.name}${colours.none}`)
+ console.log(`${last}─ ${colors.yellow}Loading event ${file.name}${colors.none}`)
const event = (await import(`../../../${config.eventsFolder}/${file.name}`));
client.on(event.event, event.callback.bind(null, client));
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${files.length}]${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${files.length}]${colors.none}`)
} catch (e) {
errors++;
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${files.length}]${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${files.length}]${colors.none}`)
}
}
console.log(`Loaded ${files.length - errors} events (${errors} failed)`)
@@ -101,36 +104,36 @@
const last = i === totalFiles - 1 ? "└" : "├";
i++;
try {
- console.log(`${last}─ ${colours.yellow}Loading message context menu ${file.name}${colours.none}`)
+ console.log(`${last}─ ${colors.yellow}Loading message context menu ${file.name}${colors.none}`)
const context = (await import(`../../../${config.messageContextFolder}/${file.name}`));
context.command.setType(ApplicationCommandType.Message);
context.command.setDMPermission(context.allowedInDMs ?? false)
context.command.setNameLocalizations(context.nameLocalizations ?? {})
commands.push(context.command);
- client.commands["contextCommands/message/" + context.command.name] = context;
+ client.commands["contextCommands/message/" + context.command.name] = [context, {name: context.name ?? context.command.name, description: context.description ?? context.command.description}];
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`)
} catch (e) {
errors++;
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}] | ${e}${colors.none}`)
}
}
for (const file of userFiles) {
const last = i === totalFiles - 1 ? "└" : "├";
i++;
try {
- console.log(`${last}─ ${colours.yellow}Loading user context menu ${file.name}${colours.none}`)
+ console.log(`${last}─ ${colors.yellow}Loading user context menu ${file.name}${colors.none}`)
const context = (await import(`../../../${config.userContextFolder}/${file.name}`));
context.command.setType(ApplicationCommandType.User);
commands.push(context.command);
client.commands["contextCommands/user/" + context.command.name] = context;
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.green}Loaded ${file.name} [${i} / ${totalFiles}]${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.green}Loaded ${file.name} [${i} / ${totalFiles}]${colors.none}`)
} catch (e) {
errors++;
- console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colours.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colours.none}`)
+ console.log(`${last.replace("└", " ").replace("├", "│")} └─ ${colors.red}Failed to load ${file.name} [${i} / ${totalFiles}]${colors.none}`)
}
}
@@ -142,11 +145,11 @@
client.on("interactionCreate", async (interaction: Interaction) => {
if (interaction.isUserContextMenuCommand()) {;
const commandName = "contextCommands/user/" + interaction.commandName;
- execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction)
+ execute(client.commands[commandName]![0]?.check, client.commands[commandName]![0]?.callback, interaction)
return;
} else if (interaction.isMessageContextMenuCommand()) {
const commandName = "contextCommands/message/" + interaction.commandName;
- execute(client.commands[commandName]?.check, client.commands[commandName]?.callback, interaction)
+ execute(client.commands[commandName]![0]?.check, client.commands[commandName]![0]?.callback, interaction)
return;
} else if (interaction.isAutocomplete()) {
const commandName = interaction.commandName;
@@ -155,7 +158,7 @@
const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : "");
- const choices = await client.commands[fullCommandName]?.autocomplete(interaction);
+ const choices = await client.commands[fullCommandName]![0]?.autocomplete(interaction);
const formatted = (choices ?? []).map(choice => {
return { name: choice, value: choice }
@@ -168,7 +171,8 @@
const fullCommandName = "commands/" + commandName + (subcommandGroupName ? `/${subcommandGroupName}` : "") + (subcommandName ? `/${subcommandName}` : "");
- const command = client.commands[fullCommandName];
+ // console.log(fullCommandName, client.commands[fullCommandName])
+ const command = client.commands[fullCommandName]![0];
const callback = command?.callback;
const check = command?.check;
execute(check, callback, interaction);
@@ -177,6 +181,7 @@
}
async function execute(check: Function | undefined, callback: Function | undefined, data: CommandInteraction) {
+ // console.log(client.commands["contextCommands/user/User info"])
if (!callback) return;
if (check) {
let result;
@@ -189,12 +194,11 @@
if (typeof result === "string") {
const { NucleusColors } = client.logger
return data.reply({embeds: [new EmojiEmbed()
- .setTitle("")
.setDescription(result)
.setColor(NucleusColors.red)
.setEmoji(getEmojiByName("CONTROL.BLOCKCROSS"))
- ]});
- };
+ ], ephemeral: true});
+ }
}
callback(data);
}
@@ -207,17 +211,20 @@
if (process.argv.includes("--update-commands")) {
if (config.enableDevelopment) {
const guild = await client.guilds.fetch(config.developmentGuildID);
- console.log(`${colours.purple}Registering commands in ${guild!.name}${colours.none}`)
+ console.log(`${colors.purple}Registering commands in ${guild!.name}${colors.none}`)
await guild.commands.set(commandList);
} else {
- console.log(`${colours.blue}Registering commands in production mode${colours.none}`)
+ console.log(`${colors.blue}Registering commands in production mode${colors.none}`)
+ const guild = await client.guilds.fetch(config.developmentGuildID);
+ await guild.commands.set([]);
await client.application?.commands.set(commandList);
}
}
await registerCommandHandler();
await registerEvents();
- console.log(`${colours.green}Registered commands, events and context menus${colours.none}`)
+ console.log(`${colors.green}Registered commands, events and context menus${colors.none}`)
console.log(
- (config.enableDevelopment ? `${colours.purple}Bot started in Development mode` :
- `${colours.blue}Bot started in Production mode`) + colours.none)
+ (config.enableDevelopment ? `${colors.purple}Bot started in Development mode` :
+ `${colors.blue}Bot started in Production mode`) + colors.none
+ )
};
diff --git a/src/utils/commandRegistration/slashCommandBuilder.ts b/src/utils/commandRegistration/slashCommandBuilder.ts
index b2927d6..66291b3 100644
--- a/src/utils/commandRegistration/slashCommandBuilder.ts
+++ b/src/utils/commandRegistration/slashCommandBuilder.ts
@@ -1,12 +1,12 @@
-import type { SlashCommandSubcommandGroupBuilder } from "@discordjs/builders";
+import { SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder } from "discord.js";
import type { SlashCommandBuilder } from "discord.js";
-import config from "../../config/main.json" assert { type: "json" };
+import config from "../../config/main.js";
import getSubcommandsInFolder from "./getFilesInFolder.js";
import client from "../client.js";
import Discord from "discord.js";
-const colours = {
+const colors = {
red: "\x1b[31m",
green: "\x1b[32m",
none: "\x1b[0m"
@@ -23,7 +23,7 @@
// If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString
console.log(`│ ├─ Loading group ${name}`)
const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path, "│ ")
- console.log(`│ │ └─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colours.none}`)
+ console.log(`│ │ └─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands for ${name} (${fetched.errors} failed)${colors.none}`)
return (subcommandGroup: SlashCommandSubcommandGroupBuilder) => {
subcommandGroup
.setName(name)
@@ -32,7 +32,9 @@
if (descriptionLocalizations) { subcommandGroup.setDescriptionLocalizations(descriptionLocalizations) }
for (const subcommand of fetched.subcommands) {
- subcommandGroup.addSubcommand(subcommand.command);
+ const processedCommand = subcommand.command(new SlashCommandSubcommandBuilder());
+ client.commands["commands/" + path + "/" + processedCommand.name] = [subcommand, { name: processedCommand.name, description: processedCommand.description }]
+ subcommandGroup.addSubcommand(processedCommand);
};
return subcommandGroup;
@@ -52,7 +54,9 @@
// If the name of the command does not match the path (e.g. attachment.ts has /attachments), use commandString
commandString = "commands/" + (commandString ?? path);
const fetched = await getSubcommandsInFolder(config.commandsFolder + "/" + path);
- console.log(`│ ├─ ${fetched.errors ? colours.red : colours.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colours.none}`)
+ console.log(`│ ├─ ${fetched.errors ? colors.red : colors.green}Loaded ${fetched.subcommands.length} subcommands and ${fetched.subcommandGroups.length} subcommand groups for ${name} (${fetched.errors} failed)${colors.none}`)
+ // console.log({name: name, description: description})
+ client.commands[commandString!] = [undefined, { name: name, description: description }]
return (command: SlashCommandBuilder) => {
command.setName(name)
command.setDescription(description)
@@ -68,15 +72,17 @@
for (const subcommand of fetched.subcommands) {
let fetchedCommand;
if (subcommand.command instanceof Function) {
- fetchedCommand = subcommand.command(new Discord.SlashCommandSubcommandBuilder());
+ fetchedCommand = subcommand.command(new SlashCommandSubcommandBuilder());
} else {
fetchedCommand = subcommand.command;
}
- client.commands[commandString! + "/" + fetchedCommand.name] = subcommand
+ client.commands[commandString! + "/" + fetchedCommand.name] = [subcommand, { name: fetchedCommand.name, description: fetchedCommand.description }]
command.addSubcommand(fetchedCommand);
}
for (const group of fetched.subcommandGroups) {
- command.addSubcommandGroup(group.command);
+ const processedCommand = group.command(new SlashCommandSubcommandGroupBuilder());
+ client.commands[commandString! + "/" + processedCommand.name] = [undefined, { name: processedCommand.name, description: processedCommand.description }]
+ command.addSubcommandGroup(processedCommand);
};
return command;
};
diff --git a/src/utils/confirmationMessage.ts b/src/utils/confirmationMessage.ts
index 4d90676..f7cccaf 100644
--- a/src/utils/confirmationMessage.ts
+++ b/src/utils/confirmationMessage.ts
@@ -1,11 +1,9 @@
-import { TextInputBuilder } from "@discordjs/builders";
+import { TextInputBuilder } from "discord.js";
import Discord, {
CommandInteraction,
- Interaction,
Message,
ActionRowBuilder,
ButtonBuilder,
- MessageComponentInteraction,
ModalSubmitInteraction,
ButtonStyle,
TextInputStyle
@@ -183,13 +181,12 @@
let component;
try {
component = await m.awaitMessageComponent({
- filter: (m) => m.user.id === this.interaction.user.id && m.channel!.id === this.interaction.channel!.id,
+ filter: (i) => i.user.id === this.interaction.user.id && i.channel!.id === this.interaction.channel!.id,
time: 300000
});
} catch (e) {
success = false;
- returnComponents = true;
- continue;
+ break;
}
if (component.customId === "yes") {
component.deferUpdate();
@@ -247,17 +244,12 @@
});
let out;
try {
- out = await modalInteractionCollector(
- m,
- (m: Interaction) =>
- (m as MessageComponentInteraction | ModalSubmitInteraction).channelId === this.interaction.channelId,
- (m) => m.customId === "reason"
- );
+ out = await modalInteractionCollector(m, this.interaction.user) as Discord.ModalSubmitInteraction | null;
} catch (e) {
cancelled = true;
continue;
}
- if (out === null) {
+ if (out === null || out.isButton()) {
cancelled = true;
continue;
}
@@ -277,23 +269,23 @@
}
const returnValue: Awaited<ReturnType<typeof this.send>> = {};
- if (returnComponents || success !== undefined) returnValue.components = this.customButtons;
- if (success !== undefined) returnValue.success = success;
if (cancelled) {
await this.timeoutError()
returnValue.cancelled = true;
}
- if (success == false) {
+ if (success === false) {
await this.interaction.editReply({
embeds: [new EmojiEmbed()
.setTitle(this.title)
- .setDescription(this.failedMessage ?? "")
+ .setDescription(this.failedMessage ?? "*Message timed out*")
.setStatus(this.failedStatus ?? "Danger")
.setEmoji(this.failedEmoji ?? this.redEmoji ?? this.emoji)
], components: []
});
return {success: false}
}
+ if (returnComponents || success !== undefined) returnValue.components = this.customButtons;
+ if (success !== undefined) returnValue.success = success;
if (newReason) returnValue.newReason = newReason;
const typedReturnValue = returnValue as {cancelled: true} |
diff --git a/src/utils/convertCurlyBracketString.ts b/src/utils/convertCurlyBracketString.ts
index 5d2c23d..058ba16 100644
--- a/src/utils/convertCurlyBracketString.ts
+++ b/src/utils/convertCurlyBracketString.ts
@@ -13,7 +13,7 @@
.replace("{member:mention}", memberID ? `<@${memberID}>` : "{member:mention}")
.replace("{member:name}", memberName ? `${memberName}` : "{member:name}")
.replace("{serverName}", serverName ? `${serverName}` : "{serverName}")
- .replace("{memberCount}", memberCount ? `${memberCount}` : "{memberCount}")
+ .replace("{memberCount:all}", memberCount ? `${memberCount}` : "{memberCount}")
.replace("{memberCount:bots}", bots ? `${bots}` : "{memberCount:bots}")
.replace("{memberCount:humans}", memberCount && bots ? `${memberCount - bots}` : "{memberCount:humans}");
diff --git a/src/utils/createPageIndicator.ts b/src/utils/createPageIndicator.ts
index ee123d6..6bc86a4 100644
--- a/src/utils/createPageIndicator.ts
+++ b/src/utils/createPageIndicator.ts
@@ -1,17 +1,17 @@
import getEmojiByName from "./getEmojiByName.js";
-function pageIndicator(amount: number, selected: number, showDetails?: boolean | true) {
+function pageIndicator(amount: number, selected: number, showDetails?: boolean, disabled?: boolean | string) {
let out = "";
-
+ disabled = disabled ? "GRAY." : ""
if (amount === 1) {
- out += getEmojiByName("TRACKS.SINGLE." + (selected === 0 ? "ACTIVE" : "INACTIVE"));
+ out += getEmojiByName("TRACKS.SINGLE." + (disabled) + (selected === 0 ? "ACTIVE" : "INACTIVE"));
} else {
for (let i = 0; i < amount; i++) {
out += getEmojiByName(
"TRACKS.HORIZONTAL." +
- (i === 0 ? "LEFT" : i === amount - 1 ? "RIGHT" : "MIDDLE") +
- "." +
- (i === selected ? "ACTIVE" : "INACTIVE")
+ (i === 0 ? "LEFT" : i === amount - 1 ? "RIGHT" : "MIDDLE") +
+ "." + (disabled) +
+ (i === selected ? "ACTIVE" : "INACTIVE")
);
}
}
@@ -21,4 +21,23 @@
return out;
}
+export const verticalTrackIndicator = (position: number, active: string | boolean, size: number, disabled: string | boolean) => {
+ active = active ? "ACTIVE" : "INACTIVE";
+ disabled = disabled ? "GRAY." : "";
+ 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;
+};
+
+export const createVerticalTrack = (items: string[], active: boolean[], disabled?: boolean[]) => {
+ let out = "";
+ if (!disabled) disabled = new Array(items.length).fill(false);
+ for (let i = 0; i < items.length; i++) {
+ out += getEmojiByName(verticalTrackIndicator(i, active[i] ?? false, items.length, disabled[i] ?? false));
+ out += items[i] + "\n";
+ }
+ return out;
+}
+
export default pageIndicator;
diff --git a/src/utils/createTemporaryStorage.ts b/src/utils/createTemporaryStorage.ts
index a684d9d..e8a8073 100644
--- a/src/utils/createTemporaryStorage.ts
+++ b/src/utils/createTemporaryStorage.ts
@@ -1,6 +1,6 @@
import client from "./client.js";
-function generalException(location: string) {
+export function generalException(location: string) {
client.noLog.push(location);
setTimeout(() => {
client.noLog = client.noLog.filter((i: string) => {
@@ -29,4 +29,4 @@
})
client.preloadPage = Object.fromEntries(object);
}, 60 * 5 * 1000);
-}
\ No newline at end of file
+}
diff --git a/src/utils/database.ts b/src/utils/database.ts
index 1e8e990..2e64320 100644
--- a/src/utils/database.ts
+++ b/src/utils/database.ts
@@ -1,32 +1,45 @@
+import { ButtonStyle, CommandInteraction, ComponentType, GuildMember, Message, MessageComponentInteraction } from "discord.js";
import type Discord from "discord.js";
import { Collection, MongoClient } from "mongodb";
-import config from "../config/main.json" assert { type: "json" };
-
-const mongoClient = new MongoClient(config.mongoUrl);
+import config from "../config/main.js";
+import client from "../utils/client.js";
+import * as crypto from "crypto";
+import _ from "lodash";
+import defaultData from '../config/default.js';
+// config.mongoOptions.host, {
+// auth: {
+// username: config.mongoOptions.username,
+// password: config.mongoOptions.password
+// },
+// authSource: config.mongoOptions.authSource
+// }
+// mongodb://emails:SweetLife2023!!@127.0.0.1:28180/saveEmail?retryWrites=true&w=majority
+const username = encodeURIComponent(config.mongoOptions.username);
+const password = encodeURIComponent(config.mongoOptions.password);
+const mongoClient = new MongoClient(username ? `mongodb://${username}:${password}@${config.mongoOptions.host}` : `mongodb://${config.mongoOptions.host}`, {authSource: "admin"});
await mongoClient.connect();
-const database = mongoClient.db("Nucleus");
+const database = mongoClient.db();
+
+const collectionOptions = { authdb: "admin" };
export class Guilds {
guilds: Collection<GuildConfig>;
- defaultData: GuildConfig | null;
+ defaultData: GuildConfig;
constructor() {
this.guilds = database.collection<GuildConfig>("guilds");
- this.defaultData = null;
- }
-
- async setup(): Promise<Guilds> {
- this.defaultData = (await import("../config/default.json", { assert: { type: "json" } }))
- .default as unknown as GuildConfig;
- return this;
+ this.defaultData = defaultData;
}
async read(guild: string): Promise<GuildConfig> {
+ // console.log("Guild read")
const entry = await this.guilds.findOne({ id: guild });
- return Object.assign({}, this.defaultData, entry);
+ const data = _.clone(this.defaultData!);
+ return _.merge(data, entry ?? {});
}
async write(guild: string, set: object | null, unset: string[] | string = []) {
+ // console.log("Guild write")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const uo: Record<string, any> = {};
if (!Array.isArray(unset)) unset = [unset];
@@ -41,6 +54,7 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async append(guild: string, key: string, value: any) {
+ // console.log("Guild append")
if (Array.isArray(value)) {
await this.guilds.updateOne(
{ id: guild },
@@ -67,7 +81,7 @@
value: any,
innerKey?: string | null
) {
- console.log(Array.isArray(value));
+ // console.log("Guild remove")
if (innerKey) {
await this.guilds.updateOne(
{ id: guild },
@@ -96,10 +110,255 @@
}
async delete(guild: string) {
+ // console.log("Guild delete")
await this.guilds.deleteOne({ id: guild });
}
}
+interface TranscriptEmbed {
+ title?: string;
+ description?: string;
+ fields?: {
+ name: string;
+ value: string;
+ inline: boolean;
+ }[];
+ footer?: {
+ text: string;
+ iconURL?: string;
+ };
+ color?: number;
+ timestamp?: string;
+ author?: {
+ name: string;
+ iconURL?: string;
+ url?: string;
+ };
+}
+
+interface TranscriptComponent {
+ type: number;
+ style?: ButtonStyle;
+ label?: string;
+ description?: string;
+ placeholder?: string;
+ emojiURL?: string;
+}
+
+interface TranscriptAuthor {
+ username: string;
+ discriminator: number;
+ nickname?: string;
+ id: string;
+ iconURL?: string;
+ topRole: {
+ color: number;
+ badgeURL?: string;
+ };
+ bot: boolean;
+}
+
+interface TranscriptAttachment {
+ url: string;
+ filename: string;
+ size: number;
+ log?: string;
+}
+
+interface TranscriptMessage {
+ id: string;
+ author: TranscriptAuthor;
+ content?: string;
+ embeds?: TranscriptEmbed[];
+ components?: TranscriptComponent[][];
+ editedTimestamp?: number;
+ createdTimestamp: number;
+ flags?: string[];
+ attachments?: TranscriptAttachment[];
+ stickerURLs?: string[];
+ referencedMessage?: string | [string, string, string]; // the message id, the channel id, the guild id
+}
+
+interface TranscriptSchema {
+ code: string;
+ for: TranscriptAuthor;
+ type: "ticket" | "purge"
+ guild: string;
+ channel: string;
+ messages: TranscriptMessage[];
+ createdTimestamp: number;
+ createdBy: TranscriptAuthor;
+}
+
+export class Transcript {
+ transcripts: Collection<TranscriptSchema>;
+
+ constructor() {
+ this.transcripts = database.collection<TranscriptSchema>("transcripts");
+ }
+
+ async create(transcript: Omit<TranscriptSchema, "code">) {
+ // console.log("Transcript create")
+ let code;
+ do {
+ code = crypto.randomBytes(64).toString("base64").replace(/=/g, "").replace(/\//g, "_").replace(/\+/g, "-");
+ } while (await this.transcripts.findOne({ code: code }));
+
+ const doc = await this.transcripts.insertOne(Object.assign(transcript, { code: code }), collectionOptions);
+ if(doc.acknowledged) return code;
+ else return null;
+ }
+
+ async read(code: string) {
+ // console.log("Transcript read")
+ return await this.transcripts.findOne({ code: code });
+ }
+
+ async createTranscript(messages: Message[], interaction: MessageComponentInteraction | CommandInteraction, member: GuildMember) {
+ const interactionMember = await interaction.guild?.members.fetch(interaction.user.id)
+ const newOut: Omit<TranscriptSchema, "code"> = {
+ type: "ticket",
+ for: {
+ username: member!.user.username,
+ discriminator: parseInt(member!.user.discriminator),
+ id: member!.user.id,
+ topRole: {
+ color: member!.roles.highest.color
+ },
+ iconURL: member!.user.displayAvatarURL({ forceStatic: true}),
+ bot: member!.user.bot
+ },
+ guild: interaction.guild!.id,
+ channel: interaction.channel!.id,
+ messages: [],
+ createdTimestamp: Date.now(),
+ createdBy: {
+ username: interaction.user.username,
+ discriminator: parseInt(interaction.user.discriminator),
+ id: interaction.user.id,
+ topRole: {
+ color: interactionMember?.roles.highest.color ?? 0x000000
+ },
+ iconURL: interaction.user.displayAvatarURL({ forceStatic: true}),
+ bot: interaction.user.bot
+ }
+ }
+ if(member.nickname) newOut.for.nickname = member.nickname;
+ if(interactionMember?.roles.icon) newOut.createdBy.topRole.badgeURL = interactionMember.roles.icon.iconURL()!;
+ messages.reverse().forEach((message) => {
+ const msg: TranscriptMessage = {
+ id: message.id,
+ author: {
+ username: message.author.username,
+ discriminator: parseInt(message.author.discriminator),
+ id: message.author.id,
+ topRole: {
+ color: message.member!.roles.highest.color
+ },
+ iconURL: message.member!.user.displayAvatarURL({ forceStatic: true}),
+ bot: message.author.bot
+ },
+ createdTimestamp: message.createdTimestamp
+ };
+ if(message.member?.nickname) msg.author.nickname = message.member.nickname;
+ if (message.member!.roles.icon) msg.author.topRole.badgeURL = message.member!.roles.icon.iconURL()!;
+ if (message.content) msg.content = message.content;
+ if (message.embeds.length > 0) msg.embeds = message.embeds.map(embed => {
+ const obj: TranscriptEmbed = {};
+ if (embed.title) obj.title = embed.title;
+ if (embed.description) obj.description = embed.description;
+ if (embed.fields.length > 0) obj.fields = embed.fields.map(field => {
+ return {
+ name: field.name,
+ value: field.value,
+ inline: field.inline ?? false
+ }
+ });
+ if (embed.color) obj.color = embed.color;
+ if (embed.timestamp) obj.timestamp = embed.timestamp
+ if (embed.footer) obj.footer = {
+ text: embed.footer.text,
+ };
+ if (embed.footer?.iconURL) obj.footer!.iconURL = embed.footer.iconURL;
+ if (embed.author) obj.author = {
+ name: embed.author.name
+ };
+ if (embed.author?.iconURL) obj.author!.iconURL = embed.author.iconURL;
+ if (embed.author?.url) obj.author!.url = embed.author.url;
+ return obj;
+ });
+ if (message.components.length > 0) msg.components = message.components.map(component => component.components.map(child => {
+ const obj: TranscriptComponent = {
+ type: child.type
+ }
+ if (child.type === ComponentType.Button) {
+ obj.style = child.style;
+ obj.label = child.label ?? "";
+ } else if (child.type > 2) {
+ obj.placeholder = child.placeholder ?? "";
+ }
+ return obj
+ }));
+ if (message.editedTimestamp) msg.editedTimestamp = message.editedTimestamp;
+ msg.flags = message.flags.toArray();
+
+ if (message.stickers.size > 0) msg.stickerURLs = message.stickers.map(sticker => sticker.url);
+ if (message.reference) msg.referencedMessage = [message.reference.guildId ?? "", message.reference.channelId, message.reference.messageId ?? ""];
+ newOut.messages.push(msg);
+ });
+ return newOut;
+ }
+
+ toHumanReadable(transcript: Omit<TranscriptSchema, "code">): string {
+ let out = "";
+ for (const message of transcript.messages) {
+ if (message.referencedMessage) {
+ if (Array.isArray(message.referencedMessage)) {
+ out += `> [Crosspost From] ${message.referencedMessage[0]} in ${message.referencedMessage[1]} in ${message.referencedMessage[2]}\n`;
+ }
+ else out += `> [Reply To] ${message.referencedMessage}\n`;
+ }
+ out += `${message.author.nickname ?? message.author.username}#${message.author.discriminator} (${message.author.id}) (${message.id})`;
+ out += ` [${new Date(message.createdTimestamp).toISOString()}]`;
+ if (message.editedTimestamp) out += ` [Edited: ${new Date(message.editedTimestamp).toISOString()}]`;
+ out += "\n";
+ if (message.content) out += `[Content]\n${message.content}\n\n`;
+ if (message.embeds) {
+ for (const embed of message.embeds) {
+ out += `[Embed]\n`;
+ if (embed.title) out += `| Title: ${embed.title}\n`;
+ if (embed.description) out += `| Description: ${embed.description}\n`;
+ if (embed.fields) {
+ for (const field of embed.fields) {
+ out += `| Field: ${field.name} - ${field.value}\n`;
+ }
+ }
+ if (embed.footer) {
+ out += `|Footer: ${embed.footer.text}\n`;
+ }
+ out += "\n";
+ }
+ }
+ if (message.components) {
+ for (const component of message.components) {
+ out += `[Component]\n`;
+ for (const button of component) {
+ out += `| Button: ${button.label ?? button.description}\n`;
+ }
+ out += "\n";
+ }
+ }
+ if (message.attachments) {
+ for (const attachment of message.attachments) {
+ out += `[Attachment] ${attachment.filename} (${attachment.size} bytes) ${attachment.url}\n`;
+ }
+ }
+ out += "\n\n"
+ }
+ return out
+ }
+}
+
export class History {
histories: Collection<HistorySchema>;
@@ -117,6 +376,7 @@
after?: string | null,
amount?: string | null
) {
+ // console.log("History create");
await this.histories.insertOne({
type: type,
guild: guild,
@@ -127,10 +387,11 @@
before: before ?? null,
after: after ?? null,
amount: amount ?? null
- });
+ }, collectionOptions);
}
async read(guild: string, user: string, year: number) {
+ // console.log("History read");
const entry = (await this.histories
.find({
guild: guild,
@@ -145,10 +406,41 @@
}
async delete(guild: string) {
+ // console.log("History delete");
await this.histories.deleteMany({ guild: guild });
}
}
+interface ScanCacheSchema {
+ addedAt: Date;
+ hash: string;
+ data: boolean;
+ tags: string[];
+}
+
+export class ScanCache {
+ scanCache: Collection<ScanCacheSchema>;
+
+ constructor() {
+ this.scanCache = database.collection<ScanCacheSchema>("scanCache");
+ }
+
+ async read(hash: string) {
+ // console.log("ScanCache read");
+ return await this.scanCache.findOne({ hash: hash });
+ }
+
+ async write(hash: string, data: boolean, tags?: string[]) {
+ // console.log("ScanCache write");
+ await this.scanCache.insertOne({ hash: hash, data: data, tags: tags ?? [], addedAt: new Date() }, collectionOptions);
+ }
+
+ async cleanup() {
+ // console.log("ScanCache cleanup");
+ await this.scanCache.deleteMany({ addedAt: { $lt: new Date(Date.now() - (1000 * 60 * 60 * 24 * 31)) }, hash: { $not$text: "http"} });
+ }
+}
+
export class PerformanceTest {
performanceData: Collection<PerformanceDataSchema>;
@@ -157,10 +449,12 @@
}
async record(data: PerformanceDataSchema) {
+ // console.log("PerformanceTest record");
data.timestamp = new Date();
- await this.performanceData.insertOne(data);
+ await this.performanceData.insertOne(data, collectionOptions);
}
async read() {
+ // console.log("PerformanceTest read");
return await this.performanceData.find({}).toArray();
}
}
@@ -184,27 +478,161 @@
}
async create(guild: string, user: string, note: string | null) {
+ // console.log("ModNotes create");
await this.modNotes.updateOne({ guild: guild, user: user }, { $set: { note: note } }, { upsert: true });
}
async read(guild: string, user: string) {
+ // console.log("ModNotes read");
const entry = await this.modNotes.findOne({ guild: guild, user: user });
return entry?.note ?? null;
}
+
+ async delete(guild: string) {
+ // console.log("ModNotes delete");
+ await this.modNotes.deleteMany({ guild: guild });
+ }
}
export class Premium {
premium: Collection<PremiumSchema>;
+ cache: Map<string, [boolean, string, number, boolean, Date]>; // Date indicates the time one hour after it was created
+ cacheTimeout = 1000 * 60 * 60; // 1 hour
constructor() {
this.premium = database.collection<PremiumSchema>("premium");
+ this.cache = new Map<string, [boolean, string, number, boolean, Date]>();
}
- async hasPremium(guild: string) {
+ async updateUser(user: string, level: number) {
+ // console.log("Premium updateUser");
+ if(!(await this.userExists(user))) await this.createUser(user, level);
+ await this.premium.updateOne({ user: user }, { $set: { level: level } }, { upsert: true });
+ }
+
+ async userExists(user: string): Promise<boolean> {
+ // console.log("Premium userExists");
+ const entry = await this.premium.findOne({ user: user });
+ return entry ? true : false;
+ }
+
+ async createUser(user: string, level: number) {
+ // console.log("Premium createUser");
+ await this.premium.insertOne({ user: user, appliesTo: [], level: level }, collectionOptions);
+ }
+
+ async hasPremium(guild: string): Promise<[boolean, string, number, boolean] | null> {
+ // console.log("Premium hasPremium");
+ // [Has premium, user giving premium, level, is mod: if given automatically]
+ const cached = this.cache.get(guild);
+ if (cached && cached[4].getTime() < Date.now()) return [cached[0], cached[1], cached[2], cached[3]];
+ const entries = await this.premium.find({}).toArray();
+ const members = (await client.guilds.fetch(guild)).members.cache
+ for(const {user} of entries) {
+ const member = members.get(user);
+ if(member) { //TODO: Notify user if they've given premium to a server that has since gotten premium via a mod.
+ const modPerms = //TODO: Create list in config for perms
+ member.permissions.has("Administrator") ||
+ member.permissions.has("ManageChannels") ||
+ member.permissions.has("ManageRoles") ||
+ member.permissions.has("ManageEmojisAndStickers") ||
+ member.permissions.has("ManageWebhooks") ||
+ member.permissions.has("ManageGuild") ||
+ member.permissions.has("KickMembers") ||
+ member.permissions.has("BanMembers") ||
+ member.permissions.has("ManageEvents") ||
+ member.permissions.has("ManageMessages") ||
+ member.permissions.has("ManageThreads")
+ const entry = entries.find(e => e.user === member.id);
+ if(entry && (entry.level === 3) && modPerms) {
+ this.cache.set(guild, [true, member.id, entry.level, true, new Date(Date.now() + this.cacheTimeout)]);
+ return [true, member.id, entry.level, true];
+ }
+ }
+ }
const entry = await this.premium.findOne({
- appliesTo: { $in: [guild] }
+ appliesTo: {
+ $elemMatch: {
+ $eq: guild
+ }
+ }
});
- return entry !== null;
+ this.cache.set(guild, [entry ? true : false, entry?.user ?? "", entry?.level ?? 0, false, new Date(Date.now() + this.cacheTimeout)]);
+ return entry ? [true, entry.user, entry.level, false] : null;
+ }
+
+ async fetchUser(user: string): Promise<PremiumSchema | null> {
+ // console.log("Premium fetchUser");
+ const entry = await this.premium.findOne({ user: user });
+ if (!entry) return null;
+ return entry;
+ }
+
+ async checkAllPremium(member?: GuildMember) {
+ // console.log("Premium checkAllPremium");
+ const entries = await this.premium.find({}).toArray();
+ if(member) {
+ const entry = entries.find(e => e.user === member.id);
+ if(entry) {
+ const expiresAt = entry.expiresAt;
+ if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: member.id}) : null;
+ }
+ const roles = member.roles;
+ let level = 0;
+ if (roles.cache.has("1066468879309750313")) {
+ level = 99;
+ } else if (roles.cache.has("1066465491713003520")) {
+ level = 1;
+ } else if (roles.cache.has("1066439526496604194")) {
+ level = 2;
+ } else if (roles.cache.has("1066464134322978912")) {
+ level = 3;
+ }
+ await this.updateUser(member.id, level);
+ if (level > 0) {
+ await this.premium.updateOne({ user: member.id }, {$unset: { expiresAt: ""}})
+ } else {
+ await this.premium.updateOne({ user: member.id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
+ }
+ } else {
+ const members = await (await client.guilds.fetch('684492926528651336')).members.fetch();
+ for(const {roles, id} of members.values()) {
+ const entry = entries.find(e => e.user === id);
+ if(entry) {
+ const expiresAt = entry.expiresAt;
+ if(expiresAt) expiresAt < Date.now() ? await this.premium.deleteOne({user: id}) : null;
+ }
+ let level: number = 0;
+ if (roles.cache.has("1066468879309750313")) {
+ level = 99;
+ } else if (roles.cache.has("1066465491713003520")) {
+ level = 1;
+ } else if (roles.cache.has("1066439526496604194")) {
+ level = 2;
+ } else if (roles.cache.has("1066464134322978912")) {
+ level = 3;
+ }
+ await this.updateUser(id, level);
+ if (level > 0) {
+ await this.premium.updateOne({ user: id }, {$unset: { expiresAt: ""}})
+ } else {
+ await this.premium.updateOne({ user: id }, {$set: { expiresAt: (Date.now() + (1000*60*60*24*3)) }})
+ }
+ }
+ }
+ }
+
+ async addPremium(user: string, guild: string) {
+ // console.log("Premium addPremium");
+ const { level } = (await this.fetchUser(user))!;
+ this.cache.set(guild, [true, user, level, false, new Date(Date.now() + this.cacheTimeout)]);
+ return this.premium.updateOne({ user: user }, { $addToSet: { appliesTo: guild } }, { upsert: true });
+ }
+
+ removePremium(user: string, guild: string) {
+ // console.log("Premium removePremium");
+ this.cache.set(guild, [false, "", 0, false, new Date(Date.now() + this.cacheTimeout)]);
+ return this.premium.updateOne({ user: user }, { $pull: { appliesTo: guild } });
}
}
@@ -249,7 +677,18 @@
channels: string[];
};
};
+ clean: {
+ channels: string[];
+ allowed: {
+ users: string[];
+ roles: string[];
+ }
+ }
};
+ autoPublish: {
+ enabled: boolean;
+ channels: string[];
+ }
welcome: {
enabled: boolean;
role: string | null;
@@ -364,6 +803,6 @@
export interface PremiumSchema {
user: string;
level: number;
- expires: Date;
appliesTo: string[];
+ expiresAt?: number;
}
diff --git a/src/utils/dualCollector.ts b/src/utils/dualCollector.ts
index 714a2d9..0b05779 100644
--- a/src/utils/dualCollector.ts
+++ b/src/utils/dualCollector.ts
@@ -1,4 +1,4 @@
-import Discord, { Client, Interaction, Message, MessageComponentInteraction } from "discord.js";
+import { ButtonInteraction, Client, User, Interaction, InteractionCollector, Message, MessageComponentInteraction, ModalSubmitInteraction } from "discord.js";
import client from "./client.js";
export default async function (
@@ -10,15 +10,14 @@
try {
out = await new Promise((resolve, _reject) => {
const mes = m
- .createMessageComponentCollector({
- filter: (m) => interactionFilter(m),
- time: 300000
- })
- .on("collect", (m) => {
+ .createMessageComponentCollector({
+ filter: (m) => interactionFilter(m),
+ time: 300000
+ })
+ .on("collect", (m) => {
resolve(m);
});
- const int = m.channel
- .createMessageCollector({
+ const int = m.channel.createMessageCollector({
filter: (m) => messageFilter(m),
time: 300000
})
@@ -45,35 +44,41 @@
return out;
}
+function defaultInteractionFilter(i: MessageComponentInteraction, user: User, m: Message) {
+ return i.channel!.id === m.channel!.id && i.user.id === user.id
+}
+function defaultModalFilter(i: ModalSubmitInteraction, user: User, m: Message) {
+ return i.channel!.id === m.channel!.id && i.user.id === user.id
+}
+
+
export async function modalInteractionCollector(
- m: Message,
- modalFilter: (i: Interaction) => boolean | Promise<boolean>,
- interactionFilter: (i: MessageComponentInteraction) => boolean | Promise<boolean>
-): Promise<null | Interaction> {
- let out: Interaction;
+ m: Message, user: User,
+ modalFilter?: (i: Interaction) => boolean | Promise<boolean>,
+ interactionFilter?: (i: MessageComponentInteraction) => boolean | Promise<boolean>
+): Promise<null | ButtonInteraction | ModalSubmitInteraction> {
+ let out: ButtonInteraction | ModalSubmitInteraction;
try {
out = await new Promise((resolve, _reject) => {
const int = m
.createMessageComponentCollector({
- filter: (i: MessageComponentInteraction) => interactionFilter(i),
+ filter: (i: MessageComponentInteraction) => (interactionFilter ? interactionFilter(i) : true) && defaultInteractionFilter(i, user, m),
time: 300000
})
- .on("collect", (i: Interaction) => {
+ .on("collect", async (i: ButtonInteraction) => {
+ mod.stop();
+ int.stop();
+ await i.deferUpdate();
resolve(i);
});
- const mod = new Discord.InteractionCollector(client as Client, {
- filter: (i: Interaction) => modalFilter(i),
+ const mod = new InteractionCollector(client as Client, {
+ filter: (i: Interaction) => (modalFilter ? modalFilter(i) : true) && i.isModalSubmit() && defaultModalFilter(i, user, m),
time: 300000
- }).on("collect", async (i: Interaction) => {
+ }).on("collect", async (i: ModalSubmitInteraction) => {
int.stop();
- (i as Discord.ModalSubmitInteraction).deferUpdate();
- resolve(i as Discord.ModalSubmitInteraction);
- });
- int.on("end", () => {
mod.stop();
- });
- mod.on("end", () => {
- int.stop();
+ await i.deferUpdate();
+ resolve(i);
});
});
} catch (e) {
diff --git a/src/utils/ellipsis.ts b/src/utils/ellipsis.ts
new file mode 100644
index 0000000..6ec5888
--- /dev/null
+++ b/src/utils/ellipsis.ts
@@ -0,0 +1,4 @@
+export default (str: string, max: number): string => {
+ if (str.length <= max) return str;
+ return str.slice(0, max - 3) + "...";
+}
\ No newline at end of file
diff --git a/src/utils/eventScheduler.ts b/src/utils/eventScheduler.ts
index 3c9d6ca..a79a260 100644
--- a/src/utils/eventScheduler.ts
+++ b/src/utils/eventScheduler.ts
@@ -2,7 +2,7 @@
import client from "./client.js";
import * as fs from "fs";
import * as path from "path";
-import config from "../config/main.json" assert { type: "json" };
+import config from "../config/main.js";
class EventScheduler {
private agenda: Agenda;
@@ -10,11 +10,10 @@
constructor() {
this.agenda = new Agenda({
db: {
- address: config.mongoUrl + "Nucleus",
+ address: config.mongoOptions.host,
collection: "eventScheduler"
}
});
-
this.agenda.define("unmuteRole", async (job) => {
const guild = await client.guilds.fetch(job.attrs.data.guild);
const user = await guild.members.fetch(job.attrs.data.user);
@@ -43,12 +42,12 @@
calculateType: "guildMemberPunish",
color: NucleusColors.green,
emoji: "PUNISH.MUTE.GREEN",
- timestamp: new Date().getTime()
+ timestamp: Date.now()
},
list: {
memberId: entry(user.user.id, `\`${user.user.id}\``),
name: entry(user.user.id, renderUser(user.user)),
- unmuted: entry(new Date().getTime().toString(), renderDelta(new Date().getTime())),
+ unmuted: entry(Date.now().toString(), renderDelta(Date.now())),
unmutedBy: entry(null, "*Time out ended*")
},
hidden: {
diff --git a/src/utils/generateEmojiEmbed.ts b/src/utils/generateEmojiEmbed.ts
index c0f17ae..a326fc5 100644
--- a/src/utils/generateEmojiEmbed.ts
+++ b/src/utils/generateEmojiEmbed.ts
@@ -1,4 +1,4 @@
-import { EmbedBuilder } from "@discordjs/builders";
+import { EmbedBuilder } from "discord.js";
import getEmojiByName from "./getEmojiByName.js";
const colors = {
@@ -13,13 +13,16 @@
description = "";
_generateTitle() {
+ if (this._emoji && !this._title) return getEmojiByName(this._emoji)
if (this._emoji) { return `${getEmojiByName(this._emoji)} ${this._title}`; }
- return this._title;
+ if (this._title) { return this._title };
+ return "";
}
override setTitle(title: string) {
this._title = title;
- super.setTitle(this._generateTitle());
+ const proposedTitle = this._generateTitle();
+ if (proposedTitle) super.setTitle(proposedTitle);
return this;
}
override setDescription(description: string) {
@@ -29,7 +32,8 @@
}
setEmoji(emoji: string) {
this._emoji = emoji;
- super.setTitle(this._generateTitle());
+ const proposedTitle = this._generateTitle();
+ if (proposedTitle) super.setTitle(proposedTitle);
return this;
}
setStatus(color: "Danger" | "Warning" | "Success") {
diff --git a/src/utils/getCommandDataByName.ts b/src/utils/getCommandDataByName.ts
new file mode 100644
index 0000000..da3e54b
--- /dev/null
+++ b/src/utils/getCommandDataByName.ts
@@ -0,0 +1,28 @@
+import type Discord from "discord.js";
+import client from "./client.js";
+
+
+export const getCommandMentionByName = (name: string): string => {
+ const split = name.replaceAll("/", " ").split(" ")
+ const commandName: string = split[0]!;
+
+ const filterCommand = (command: Discord.ApplicationCommand) => command.name === commandName;
+
+ const command = client.fetchedCommands.filter(c => filterCommand(c))
+ if (command.size === 0) return `\`/${name.replaceAll("/", " ")}\``;
+ const commandID = command.first()!.id;
+ return `</${split.join(" ")}:${commandID}>`;
+}
+
+export const getCommandByName = (name: string): {name: string, description: string, mention: string} => {
+
+ const split = name.replaceAll(" ", "/")
+ const command = client.commands["commands/" + split]!;
+ // console.log(command)
+ const mention = getCommandMentionByName(name);
+ return {
+ name: command[1].name,
+ description: command[1].description,
+ mention: mention
+ }
+}
\ No newline at end of file
diff --git a/src/utils/getCommandMentionByName.ts b/src/utils/getCommandMentionByName.ts
deleted file mode 100644
index b2b9937..0000000
--- a/src/utils/getCommandMentionByName.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type Discord from "discord.js";
-import client from "./client.js";
-import config from "../config/main.json" assert { type: "json"};
-
-
-export const getCommandMentionByName = async (name: string): Promise<string> => {
- const split = name.replaceAll("/", " ").split(" ")
- const commandName: string = split[0]!;
- let commandID: string;
-
- const filterCommand = (command: Discord.ApplicationCommand) => command.name === commandName;
-
- if (config.enableDevelopment) {
- const developmentGuild = client.guilds.cache.get(config.developmentGuildID)!;
- await developmentGuild.commands.fetch();
- commandID = developmentGuild.commands.cache.filter(c => filterCommand(c)).first()!.id;
- } else {
- await client.application?.commands.fetch();
- commandID = client.application?.commands.cache.filter(c => filterCommand(c)).first()!.id!;
- }
- return `</${split.join(" ")}:${commandID}>`;
-}
\ No newline at end of file
diff --git a/src/utils/getEmojiByName.ts b/src/utils/getEmojiByName.ts
index 3fa2b53..9df17a4 100644
--- a/src/utils/getEmojiByName.ts
+++ b/src/utils/getEmojiByName.ts
@@ -1,5 +1,7 @@
import emojis from "../config/emojis.json" assert { type: "json" };
+import lodash from 'lodash';
+const isArray = lodash.isArray;
interface EmojisIndex {
[key: string]: string | EmojisIndex | EmojisIndex[];
}
@@ -12,7 +14,7 @@
if (typeof id === "string" || id === undefined) {
throw new Error(`Emoji ${name} not found`);
}
- if (Array.isArray(id)) {
+ if (isArray(id)) {
id = id[parseInt(part)];
} else {
id = id[part];
@@ -21,6 +23,10 @@
if (typeof id !== "string" && id !== undefined) {
throw new Error(`Emoji ${name} not found`);
}
+ return getEmojiFromId(id, format);
+}
+
+function getEmojiFromId(id: string | undefined, format?: string): string {
if (format === "id") {
if (id === undefined) return "0";
return id.toString();
diff --git a/src/utils/listToAndMore.ts b/src/utils/listToAndMore.ts
new file mode 100644
index 0000000..791ce40
--- /dev/null
+++ b/src/utils/listToAndMore.ts
@@ -0,0 +1,7 @@
+export default (list: string[], max: number) => {
+ // PineappleFan, Coded, Mini (and 10 more)
+ if(list.length > max) {
+ return list.slice(0, max).join(", ") + ` (and ${list.length - max} more)`;
+ }
+ return list.join(", ");
+}
\ No newline at end of file
diff --git a/src/utils/log.ts b/src/utils/log.ts
index 54f656a..c6416a1 100644
--- a/src/utils/log.ts
+++ b/src/utils/log.ts
@@ -7,10 +7,37 @@
const wait = promisify(setTimeout);
+export interface LoggerOptions {
+ meta: {
+ type: string;
+ displayName: string;
+ calculateType: string;
+ color: number;
+ emoji: string;
+ timestamp: number;
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ list: any;
+ hidden: {
+ guild: string;
+ },
+ separate?: {
+ start?: string;
+ end?: string;
+ }
+}
+
+async function isLogging(guild: string, type: string): Promise<boolean> {
+ const config = await client.database.guilds.read(guild);
+ if (!config.logging.logs.enabled) return false;
+ if (!config.logging.logs.channel) return false;
+ if (!toHexArray(config.logging.logs.toLog).includes(type)) { return false; }
+ return true;
+}
export const Logger = {
renderUser(user: Discord.User | string) {
- if (typeof user === "string") return `${user} [<@${user}>]`;
+ if (typeof user === "string") user = client.users.cache.get(user)!;
return `${user.username} [<@${user.id}>]`;
},
renderTime(t: number) {
@@ -29,10 +56,12 @@
if (typeof value === "number") value = value.toString();
return { value: value, displayValue: displayValue };
},
- renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel) {
+ renderChannel(channel: Discord.GuildChannel | Discord.ThreadChannel | string) {
+ if (typeof channel === "string") channel = client.channels.cache.get(channel) as Discord.GuildChannel | Discord.ThreadChannel;
return `${channel.name} [<#${channel.id}>]`;
},
- renderRole(role: Discord.Role) {
+ renderRole(role: Discord.Role | string, guild?: Discord.Guild | string) {
+ if (typeof role === "string") role = (typeof guild === "string" ? client.guilds.cache.get(guild) : guild)!.roles.cache.get(role)!;
return `${role.name} [<@&${role.id}>]`;
},
renderEmoji(emoji: Discord.GuildEmoji) {
@@ -43,19 +72,14 @@
yellow: 0xf2d478,
green: 0x68d49e
},
- async getAuditLog(guild: Discord.Guild, event: Discord.GuildAuditLogsResolvable): Promise<Discord.GuildAuditLogsEntry[]> {
- await wait(250);
+ async getAuditLog(guild: Discord.Guild, event: Discord.GuildAuditLogsResolvable, delay?: number): Promise<Discord.GuildAuditLogsEntry[]> {
+ await wait(delay ?? 250);
const auditLog = (await guild.fetchAuditLogs({ type: event })).entries.map(m => m)
return auditLog as Discord.GuildAuditLogsEntry[];
},
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- async log(log: any): Promise<void> {
+ async log(log: LoggerOptions): Promise<void> {
+ if (!await isLogging(log.hidden.guild, log.meta.calculateType)) return;
const config = await client.database.guilds.read(log.hidden.guild);
- if (!config.logging.logs.enabled) return;
- if (!toHexArray(config.logging.logs.toLog).includes(log.meta.calculateType)) {
- console.log("Not logging this type of event");
- return;
- }
if (config.logging.logs.channel) {
const channel = (await client.channels.fetch(config.logging.logs.channel)) as Discord.TextChannel | null;
const description: Record<string, string> = {};
@@ -70,7 +94,7 @@
}
});
if (channel) {
- log.separate = log.separate || {};
+ log.separate = log.separate ?? {};
const embed = new Discord.EmbedBuilder()
.setTitle(`${getEmojiByName(log.meta.emoji)} ${log.meta.displayName}`)
.setDescription(
@@ -83,8 +107,8 @@
channel.send({ embeds: [embed] });
}
}
- }
+ },
+ isLogging
};
-
export default {};
diff --git a/src/utils/logTranscripts.ts b/src/utils/logTranscripts.ts
deleted file mode 100644
index 0950664..0000000
--- a/src/utils/logTranscripts.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import type Discord from 'discord.js';
-
-export interface JSONTranscriptSchema {
- messages: {
- content: string | null;
- attachments: {
- url: string;
- name: string;
- size: number;
- }[];
- authorID: string;
- authorUsername: string;
- authorUsernameColor: string;
- timestamp: string;
- id: string;
- edited: boolean;
- }[];
- channel: string;
- guild: string;
- timestamp: string;
-}
-
-
-export const JSONTranscriptFromMessageArray = (messages: Discord.Message[]): JSONTranscriptSchema | null => {
- if (messages.length === 0) return null;
- return {
- guild: messages[0]!.guild!.id,
- channel: messages[0]!.channel.id,
- timestamp: Date.now().toString(),
- messages: messages.map((message: Discord.Message) => {
- return {
- content: message.content,
- attachments: message.attachments.map((attachment: Discord.Attachment) => {
- return {
- url: attachment.url,
- name: attachment.name!,
- size: attachment.size,
- };
- }),
- authorID: message.author.id,
- authorUsername: message.author.username + "#" + message.author.discriminator,
- authorUsernameColor: message.member!.displayHexColor.toString(),
- timestamp: message.createdTimestamp.toString(),
- id: message.id,
- edited: message.editedTimestamp ? true : false,
- };
- })
- };
-}
-
-export const JSONTranscriptToHumanReadable = (data: JSONTranscriptSchema): string => {
- let out = "";
-
- for (const message of data.messages) {
- const date = new Date(parseInt(message.timestamp));
- out += `${message.authorUsername} (${message.authorID}) [${date}]`;
- if (message.edited) out += " (edited)";
- if (message.content) out += "\nContent:\n" + message.content.split("\n").map((line: string) => `\n> ${line}`).join("");
- if (message.attachments.length > 0) out += "\nAttachments:\n" + message.attachments.map((attachment: { url: string; name: string; size: number; }) => `\n> [${attachment.name}](${attachment.url}) (${attachment.size} bytes)`).join("\n");
-
- out += "\n\n";
- }
- return out;
-}
\ No newline at end of file
diff --git a/src/utils/memory.ts b/src/utils/memory.ts
index 870ffaf..60a6535 100644
--- a/src/utils/memory.ts
+++ b/src/utils/memory.ts
@@ -7,6 +7,7 @@
logging: GuildConfig["logging"];
tickets: GuildConfig["tickets"];
tags: GuildConfig["tags"];
+ autoPublish: GuildConfig["autoPublish"];
}
class Memory {
@@ -31,7 +32,8 @@
filters: guildData.filters,
logging: guildData.logging,
tickets: guildData.tickets,
- tags: guildData.tags
+ tags: guildData.tags,
+ autoPublish: guildData.autoPublish
});
}
return this.memory.get(guild)!;
diff --git a/src/utils/performanceTesting/record.ts b/src/utils/performanceTesting/record.ts
index 95761e9..71883c5 100644
--- a/src/utils/performanceTesting/record.ts
+++ b/src/utils/performanceTesting/record.ts
@@ -2,7 +2,7 @@
import * as CP from 'child_process';
import * as process from 'process';
import systeminformation from "systeminformation";
-import config from "../../config/main.json" assert { type: "json" };
+import config from "../../config/main.js";
import singleNotify from "../singleNotify.js";
@@ -39,7 +39,7 @@
singleNotify(
"performanceTest",
config.developmentGuildID,
- `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${results.resources.memory}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``,
+ `Discord ping time: \`${results.discord}ms\`\nDatabase read time: \`${results.databaseRead}ms\`\nCPU usage: \`${results.resources.cpu}%\`\nMemory usage: \`${Math.round(results.resources.memory)}MB\`\nCPU temperature: \`${results.resources.temperature}°C\``,
"Critical",
config.owners
)
diff --git a/src/utils/singleNotify.ts b/src/utils/singleNotify.ts
index 8e3aa60..6bf63e1 100644
--- a/src/utils/singleNotify.ts
+++ b/src/utils/singleNotify.ts
@@ -1,6 +1,7 @@
import client from "./client.js";
import EmojiEmbed from "./generateEmojiEmbed.js";
import { Record as ImmutableRecord } from "immutable";
+import type { TextChannel, ThreadChannel, NewsChannel } from "discord.js";
const severitiesType = ImmutableRecord({
Critical: "Danger",
@@ -31,20 +32,20 @@
const channel = await client.channels.fetch(data.logging.staff.channel);
if (!channel) return;
if (!channel.isTextBased()) return;
+ const textChannel = channel as TextChannel | ThreadChannel | NewsChannel;
+ let messageData = {embeds: [
+ new EmojiEmbed()
+ .setTitle(`${severity} notification`)
+ .setDescription(message)
+ .setStatus(severities.get(severity))
+ .setEmoji("CONTROL.BLOCKCROSS")
+ ]}
if (pings) {
- await channel.send({
+ messageData = Object.assign(messageData, {
content: pings.map((ping) => `<@${ping}>`).join(" ")
});
}
- await channel.send({
- embeds: [
- new EmojiEmbed()
- .setTitle(`${severity} notification`)
- .setDescription(message)
- .setStatus(severities.get(severity))
- .setEmoji("CONTROL.BLOCKCROSS")
- ]
- });
+ await textChannel.send(messageData);
} catch (err) {
console.error(err);
}
diff --git a/src/utils/temp/generateFileName.ts b/src/utils/temp/generateFileName.ts
index 3aab64c..109478d 100644
--- a/src/utils/temp/generateFileName.ts
+++ b/src/utils/temp/generateFileName.ts
@@ -12,7 +12,7 @@
if (fs.existsSync(`./${fileName}`)) {
fileName = generateFileName(ending);
}
- client.database.eventScheduler.schedule("deleteFile", (new Date().getTime() + 60 * 1000).toString(), {
+ client.database.eventScheduler.schedule("deleteFile", (Date.now() + 60 * 1000).toString(), {
fileName: `${fileName}.${ending}`
});
return path.join(__dirname, fileName + "." + ending);