blob: 2fd947b60c0dba9edb50e85531700c50c4073617 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { LoadingEmbed } from "../../utils/defaults.js";
TheCodedProf9bc84752023-01-21 05:19:57 -05002import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, TextInputBuilder, StringSelectMenuInteraction, ButtonInteraction, MessageComponentInteraction, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction } from "discord.js";
pineafan0bc04162022-07-25 17:22:26 +01003import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
TheCodedProfafca98b2023-01-17 22:25:43 -05004import type { SlashCommandSubcommandBuilder } from "@discordjs/builders";
pineafan0bc04162022-07-25 17:22:26 +01005import client from "../../utils/client.js";
pineafan63fc5e22022-08-04 22:04:10 +01006import convertCurlyBracketString from "../../utils/convertCurlyBracketString.js";
pineafan63fc5e22022-08-04 22:04:10 +01007import singleNotify from "../../utils/singleNotify.js";
TheCodedProf4a6d5712023-01-19 15:54:40 -05008import getEmojiByName from "../../utils/getEmojiByName.js";
9import createPageIndicator from "../../utils/createPageIndicator.js";
10import { modalInteractionCollector } from "../../utils/dualCollector.js";
TheCodedProfc2acbcc2023-01-20 17:23:51 -050011
pineafan708692b2022-07-24 22:16:22 +010012
13const command = (builder: SlashCommandSubcommandBuilder) =>
14 builder
pineafan63fc5e22022-08-04 22:04:10 +010015 .setName("stats")
Skyler Grey11236ba2022-08-08 21:13:33 +010016 .setDescription("Controls channels which update when someone joins or leaves the server")
pineafan708692b2022-07-24 22:16:22 +010017
TheCodedProf4a6d5712023-01-19 15:54:40 -050018
TheCodedProf9bc84752023-01-21 05:19:57 -050019const showModal = async (interaction: MessageComponentInteraction, current: { enabled: boolean; name: string; }) => {
TheCodedProfc2acbcc2023-01-20 17:23:51 -050020 await interaction.showModal(
21 new Discord.ModalBuilder()
22 .setCustomId("modal")
23 .setTitle(`Stats channel name`)
24 .addComponents(
25 new ActionRowBuilder<TextInputBuilder>().addComponents(
26 new TextInputBuilder()
27 .setCustomId("ex1")
28 .setLabel("Server Info (1/3)")
29 .setPlaceholder(
30 `{serverName} - This server's name\n\n` +
31 `These placeholders will be replaced with the server's name, etc..`
32 )
33 .setMaxLength(1)
34 .setRequired(false)
35 .setStyle(Discord.TextInputStyle.Paragraph)
36 ),
37 new ActionRowBuilder<TextInputBuilder>().addComponents(
38 new TextInputBuilder()
39 .setCustomId("ex2")
40 .setLabel("Member Counts (2/3) - {MemberCount:...}")
41 .setPlaceholder(
42 `{:all} - Total member count\n` +
43 `{:humans} - Total non-bot users\n` +
44 `{:bots} - Number of bots\n`
45 )
46 .setMaxLength(1)
47 .setRequired(false)
48 .setStyle(Discord.TextInputStyle.Paragraph)
49 ),
50 new ActionRowBuilder<TextInputBuilder>().addComponents(
51 new TextInputBuilder()
52 .setCustomId("ex3")
53 .setLabel("Latest Member (3/3) - {member:...}")
54 .setPlaceholder(
55 `{:name} - The members name\n`
56 )
57 .setMaxLength(1)
58 .setRequired(false)
59 .setStyle(Discord.TextInputStyle.Paragraph)
60 ),
61 new ActionRowBuilder<TextInputBuilder>().addComponents(
62 new TextInputBuilder()
63 .setCustomId("text")
64 .setLabel("Channel name input")
65 .setMaxLength(1000)
66 .setRequired(true)
67 .setStyle(Discord.TextInputStyle.Short)
68 .setValue(current.name)
69 )
70 )
71 );
TheCodedProf4a6d5712023-01-19 15:54:40 -050072}
73
TheCodedProf9bc84752023-01-21 05:19:57 -050074type ObjectSchema = Record<string, {name: string, enabled: boolean}>
75
TheCodedProf267563a2023-01-21 17:00:57 -050076
TheCodedProf9bc84752023-01-21 05:19:57 -050077
78const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise<ObjectSchema> => {
79 let closed = false;
80 let cancelled = false;
81 const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}]));
82 let newChannel: string | undefined;
83 let newChannelName: string = "{memberCount:all}-members";
84 let newChannelEnabled: boolean = true;
85 do {
TheCodedProf267563a2023-01-21 17:00:57 -050086 m = await interaction.editReply({
TheCodedProf9bc84752023-01-21 05:19:57 -050087 embeds: [new EmojiEmbed()
88 .setTitle("Stats Channel")
89 .setDescription(
90 `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" +
91 `**Name:** \`${newChannelName}\`\n` +
92 `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` +
93 `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n`
94 )
95 .setEmoji("SETTINGS.STATS.GREEN")
96 .setStatus("Success")
97 ], components: [
98 new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
99 new ChannelSelectMenuBuilder()
100 .setCustomId("channel")
TheCodedProf267563a2023-01-21 17:00:57 -0500101 .setPlaceholder("Select a channel to use")
TheCodedProf9bc84752023-01-21 05:19:57 -0500102 ),
103 new ActionRowBuilder<ButtonBuilder>().addComponents(
104 new ButtonBuilder()
105 .setLabel("Cancel")
106 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
107 .setStyle(ButtonStyle.Danger)
108 .setCustomId("back"),
109 new ButtonBuilder()
110 .setLabel("Save")
111 .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
112 .setStyle(ButtonStyle.Success)
113 .setCustomId("save"),
114 new ButtonBuilder()
115 .setLabel("Edit name")
116 .setEmoji(getEmojiByName("ICONS.EDIT", "id"))
117 .setStyle(ButtonStyle.Primary)
118 .setCustomId("editName"),
119 new ButtonBuilder()
120 .setLabel(newChannelEnabled ? "Enabled" : "Disabled")
121 .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id"))
122 .setStyle(ButtonStyle.Secondary)
123 .setCustomId("toggleEnabled")
124 )
125 ]
126 });
127 let i: ButtonInteraction | ChannelSelectMenuInteraction;
128 try {
TheCodedProf267563a2023-01-21 17:00:57 -0500129 i = await m.awaitMessageComponent({ time: 300000, filter: (i) => {
130 return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id;
131 }}) as ButtonInteraction | ChannelSelectMenuInteraction;
TheCodedProf9bc84752023-01-21 05:19:57 -0500132 } catch (e) {
133 closed = true;
134 cancelled = true;
135 break;
136 }
137 if (i.isButton()) {
138 switch (i.customId) {
139 case "back":
TheCodedProf267563a2023-01-21 17:00:57 -0500140 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500141 closed = true;
142 break;
143 case "save":
TheCodedProf267563a2023-01-21 17:00:57 -0500144 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500145 if (newChannel) {
146 currentObject[newChannel] = {
147 name: newChannelName,
148 enabled: newChannelEnabled
149 }
150 }
151 closed = true;
152 break;
153 case "editName":
154 await interaction.editReply({
155 embeds: [new EmojiEmbed()
156 .setTitle("Stats Channel")
157 .setDescription("Modal opened. If you can't see it, click back and try again.")
158 .setStatus("Success")
159 .setEmoji("SETTINGS.STATS.GREEN")
160 ],
161 components: [
162 new ActionRowBuilder<ButtonBuilder>().addComponents(
163 new ButtonBuilder()
164 .setLabel("Back")
165 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
166 .setStyle(ButtonStyle.Primary)
167 .setCustomId("back")
168 )
169 ]
170 });
171 showModal(i, {name: newChannelName, enabled: newChannelEnabled})
172
173 const out = await modalInteractionCollector(
174 m,
TheCodedProf267563a2023-01-21 17:00:57 -0500175 (m) => m.channel!.id === interaction.channel!.id && m.user!.id === interaction.user!.id,
176 (i) => i.channel!.id === interaction.channel!.id && i.user!.id === interaction.user!.id && i.message!.id === m.id
TheCodedProf9bc84752023-01-21 05:19:57 -0500177 ) as Discord.ModalSubmitInteraction | null;
178 if (!out) continue;
179 if (!out.fields) continue;
180 if (out.isButton()) continue;
181 newChannelName = out.fields.getTextInputValue("text");
182 break;
183 case "toggleEnabled":
TheCodedProf267563a2023-01-21 17:00:57 -0500184 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500185 newChannelEnabled = !newChannelEnabled;
186 break;
187 }
188 } else {
TheCodedProf267563a2023-01-21 17:00:57 -0500189 await i.deferUpdate();
TheCodedProf9bc84752023-01-21 05:19:57 -0500190 if (i.customId === "channel") {
191 newChannel = i.values[0];
192 }
193 }
194 } while (!closed)
195 if (cancelled) return originalObject;
196 if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject;
TheCodedProf267563a2023-01-21 17:00:57 -0500197 return currentObject;
TheCodedProf9bc84752023-01-21 05:19:57 -0500198}
PineaFan5d98a4b2023-01-19 16:15:47 +0000199const callback = async (interaction: CommandInteraction) => {
200 if (!interaction.guild) return;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500201 const { renderChannel } = client.logger;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500202 const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500203 let page = 0;
204 let closed = false;
205 const config = await client.database.guilds.read(interaction.guild.id);
TheCodedProf9bc84752023-01-21 05:19:57 -0500206 let currentObject: ObjectSchema = config.stats;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500207 let modified = false;
PineaFan5d98a4b2023-01-19 16:15:47 +0000208 do {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500209 let embed = new EmojiEmbed()
210 .setTitle("Stats Settings")
211 .setEmoji("SETTINGS.STATS.GREEN")
212 .setStatus("Success");
213 const noStatsChannels = Object.keys(currentObject).length === 0;
214 let current: { enabled: boolean; name: string; };
215
216 const pageSelect = new StringSelectMenuBuilder()
PineaFan5d98a4b2023-01-19 16:15:47 +0000217 .setCustomId("page")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500218 .setPlaceholder("Select a stats channel to manage");
219 const actionSelect = new StringSelectMenuBuilder()
TheCodedProf4a6d5712023-01-19 15:54:40 -0500220 .setCustomId("action")
221 .setPlaceholder("Perform an action")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500222 .addOptions(
223 new StringSelectMenuOptionBuilder()
224 .setLabel("Edit")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500225 .setDescription("Edit the stats channel")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500226 .setValue("edit")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500227 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
228 new StringSelectMenuOptionBuilder()
229 .setLabel("Delete")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500230 .setDescription("Delete the stats channel")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500231 .setValue("delete")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500232 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500233 );
234 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
235 .addComponents(
236 new ButtonBuilder()
237 .setCustomId("back")
238 .setStyle(ButtonStyle.Primary)
239 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
240 .setDisabled(page === 0),
241 new ButtonBuilder()
242 .setCustomId("next")
243 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
244 .setStyle(ButtonStyle.Primary)
245 .setDisabled(page === Object.keys(currentObject).length - 1),
246 new ButtonBuilder()
247 .setCustomId("add")
248 .setLabel("Create new")
249 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
250 .setStyle(ButtonStyle.Secondary)
251 .setDisabled(Object.keys(currentObject).length >= 24),
252 new ButtonBuilder()
253 .setCustomId("save")
254 .setLabel("Save")
255 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
256 .setStyle(ButtonStyle.Success)
257 .setDisabled(modified),
TheCodedProf4a6d5712023-01-19 15:54:40 -0500258 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500259 if (noStatsChannels) {
260 embed.setDescription("No stats channels have been set up yet. Use the button below to add one.\n\n" +
261 createPageIndicator(1, 1, undefined, true)
262 );
263 pageSelect.setDisabled(true);
264 actionSelect.setDisabled(true);
265 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
266 .setLabel("No stats channels")
267 .setValue("none")
268 );
TheCodedProf4a6d5712023-01-19 15:54:40 -0500269 } else {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500270 page = Math.min(page, Object.keys(currentObject).length - 1);
271 current = currentObject[Object.keys(config.stats)[page]!]!
TheCodedProf4a6d5712023-01-19 15:54:40 -0500272 actionSelect.addOptions(new StringSelectMenuOptionBuilder()
273 .setLabel(current.enabled ? "Disable" : "Enable")
274 .setValue("toggleEnabled")
275 .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
276 .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
277 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500278
279 embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` +
TheCodedProf4a6d5712023-01-19 15:54:40 -0500280 `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
281 `**Name:** \`${current.name}\`\n` +
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500282 `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' +
283 createPageIndicator(Object.keys(config.stats).length, page)
TheCodedProf4a6d5712023-01-19 15:54:40 -0500284 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500285 for (const [id, { name, enabled }] of Object.entries(currentObject)) {
286 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
287 .setLabel(`${name} (${renderChannel(id)})`)
288 .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
289 .setDescription(`${enabled ? "Enabled" : "Disabled"}`)
290 .setValue(id)
291 );
292 }
293 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500294
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500295 interaction.editReply({embeds: [embed], components: [
296 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
297 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
298 buttonRow
299 ]});
TheCodedProf4a6d5712023-01-19 15:54:40 -0500300
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500301 let i: StringSelectMenuInteraction | ButtonInteraction;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500302 try {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500303 i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500304 } catch (e) {
305 closed = true;
306 continue;
307 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500308
309 if(i.isStringSelectMenu()) {
TheCodedProf4a6d5712023-01-19 15:54:40 -0500310 switch(i.customId) {
311 case "page":
TheCodedProf267563a2023-01-21 17:00:57 -0500312 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500313 page = Object.keys(currentObject).indexOf(i.values[0]!);
TheCodedProf4a6d5712023-01-19 15:54:40 -0500314 break;
315 case "action":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500316 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500317 switch(i.values[0]!) {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500318 case "edit": {
319 showModal(i, current!)
TheCodedProf4a6d5712023-01-19 15:54:40 -0500320 await interaction.editReply({
321 embeds: [
322 new EmojiEmbed()
323 .setTitle("Stats Channel")
324 .setDescription("Modal opened. If you can't see it, click back and try again.")
325 .setStatus("Success")
326 .setEmoji("SETTINGS.STATS.GREEN")
327 ],
328 components: [
329 new ActionRowBuilder<ButtonBuilder>().addComponents(
330 new ButtonBuilder()
331 .setLabel("Back")
332 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
333 .setStyle(ButtonStyle.Primary)
334 .setCustomId("back")
335 )
336 ]
337 });
TheCodedProf9bc84752023-01-21 05:19:57 -0500338 let out: Discord.ModalSubmitInteraction | null;
339 try {
340 out = await modalInteractionCollector(
341 m,
342 (m) => m.channel!.id === interaction.channel!.id,
343 (_) => true
344 ) as Discord.ModalSubmitInteraction | null;
345 } catch (e) {
346 continue;
347 }
348 if (!out) continue
349 if (!out.fields) continue
350 if (out.isButton()) continue;
351 currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text");
TheCodedProf4a6d5712023-01-19 15:54:40 -0500352 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500353 }
354 case "toggleEnabled": {
TheCodedProf267563a2023-01-21 17:00:57 -0500355 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500356 currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled;
357 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500358 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500359 }
360 case "delete": {
TheCodedProf267563a2023-01-21 17:00:57 -0500361 await i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500362 delete currentObject[Object.keys(currentObject)[page]!];
TheCodedProf267563a2023-01-21 17:00:57 -0500363 page = Math.min(page, Object.keys(currentObject).length - 1);
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500364 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500365 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500366 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500367 }
368 break;
369 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500370 } else {
TheCodedProf267563a2023-01-21 17:00:57 -0500371 await i.deferUpdate();
TheCodedProf4a6d5712023-01-19 15:54:40 -0500372 switch(i.customId) {
373 case "back":
374 page--;
375 break;
376 case "next":
377 page++;
378 break;
379 case "add":
TheCodedProf9bc84752023-01-21 05:19:57 -0500380 currentObject = await addStatsChannel(interaction, m, currentObject);
TheCodedProf267563a2023-01-21 17:00:57 -0500381 page = Object.keys(currentObject).length - 1;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500382 break;
383 case "save":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500384 client.database.guilds.write(interaction.guild.id, {stats: currentObject});
385 singleNotify("statsChannelDeleted", interaction.guild.id, true);
386 modified = false;
387 break;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500388 }
389 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500390
PineaFan5d98a4b2023-01-19 16:15:47 +0000391 } while (!closed);
pineafan63fc5e22022-08-04 22:04:10 +0100392};
pineafan708692b2022-07-24 22:16:22 +0100393
PineaFan64486c42022-12-28 09:21:04 +0000394const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100395 const member = interaction.member as Discord.GuildMember;
TheCodedProfafca98b2023-01-17 22:25:43 -0500396 if (!member.permissions.has("ManageChannels"))
PineaFan0d06edc2023-01-17 22:10:31 +0000397 return "You must have the *Manage Channels* permission to use this command";
pineafan708692b2022-07-24 22:16:22 +0100398 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100399};
pineafan708692b2022-07-24 22:16:22 +0100400
PineaFan538d3752023-01-12 21:48:23 +0000401
pineafan708692b2022-07-24 22:16:22 +0100402export { command };
403export { callback };
PineaFan5d98a4b2023-01-19 16:15:47 +0000404export { check };