blob: d46b57e01d57caf9f6bb07d2863f7c77d011cbd8 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { LoadingEmbed } from "../../utils/defaults.js";
Samuel Shuert27bf3cd2023-03-03 15:51:25 -05002import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, TextInputBuilder, StringSelectMenuInteraction, ButtonInteraction, MessageComponentInteraction, ChannelSelectMenuBuilder, ChannelSelectMenuInteraction, ModalBuilder } from "discord.js";
pineafan0bc04162022-07-25 17:22:26 +01003import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
Samuel Shuert27bf3cd2023-03-03 15:51:25 -05004import type { SlashCommandSubcommandBuilder } from "discord.js";
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";
Samuel Shuert27bf3cd2023-03-03 15:51:25 -05008import getEmojiByName from "../../utils/getEmojiByName.js";
9import createPageIndicator from "../../utils/createPageIndicator.js";
10import { modalInteractionCollector } from "../../utils/dualCollector.js";
11
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
Samuel Shuert27bf3cd2023-03-03 15:51:25 -050018
19const showModal = async (interaction: MessageComponentInteraction, current: { enabled: boolean; name: string; }) => {
20 await interaction.showModal(
21 new 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..`
Skyler Grey75ea9172022-08-06 10:22:23 +010032 )
Samuel Shuert27bf3cd2023-03-03 15:51:25 -050033 .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 );
72}
73
74type ObjectSchema = Record<string, {name: string, enabled: boolean}>
75
76
77const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise<ObjectSchema> => {
78 let closed = false;
79 let cancelled = false;
80 const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}]));
81 let newChannel: string | undefined;
82 let newChannelName: string = "{memberCount:all}-members";
83 let newChannelEnabled: boolean = true;
84 do {
85 m = await interaction.editReply({
86 embeds: [new EmojiEmbed()
87 .setTitle("Stats Channel")
88 .setDescription(
89 `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" +
90 `**Name:** \`${newChannelName}\`\n` +
91 `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` +
92 `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n`
93 )
94 .setEmoji("SETTINGS.STATS.GREEN")
95 .setStatus("Success")
96 ], components: [
97 new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
98 new ChannelSelectMenuBuilder()
99 .setCustomId("channel")
100 .setPlaceholder("Select a channel to use")
101 ),
102 new ActionRowBuilder<ButtonBuilder>().addComponents(
103 new ButtonBuilder()
104 .setLabel("Cancel")
105 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
106 .setStyle(ButtonStyle.Danger)
107 .setCustomId("back"),
108 new ButtonBuilder()
109 .setLabel("Save")
110 .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
111 .setStyle(ButtonStyle.Success)
112 .setCustomId("save"),
113 new ButtonBuilder()
114 .setLabel("Edit name")
115 .setEmoji(getEmojiByName("ICONS.EDIT", "id"))
116 .setStyle(ButtonStyle.Primary)
117 .setCustomId("editName"),
118 new ButtonBuilder()
119 .setLabel(newChannelEnabled ? "Enabled" : "Disabled")
120 .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id"))
121 .setStyle(ButtonStyle.Secondary)
122 .setCustomId("toggleEnabled")
Skyler Grey75ea9172022-08-06 10:22:23 +0100123 )
124 ]
125 });
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500126 let i: ButtonInteraction | ChannelSelectMenuInteraction;
pineafan0bc04162022-07-25 17:22:26 +0100127 try {
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500128 i = await m.awaitMessageComponent({ time: 300000, filter: (i) => {
129 return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id;
130 }}) as ButtonInteraction | ChannelSelectMenuInteraction;
Skyler Grey75ea9172022-08-06 10:22:23 +0100131 } catch (e) {
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500132 closed = true;
133 cancelled = true;
134 break;
135 }
136 if (i.isButton()) {
137 switch (i.customId) {
138 case "back": {
139 await i.deferUpdate();
140 closed = true;
141 break;
142 }
143 case "save": {
144 await i.deferUpdate();
145 if (newChannel) {
146 currentObject[newChannel] = {
147 name: newChannelName,
148 enabled: newChannelEnabled
149 }
150 }
151 closed = true;
152 break;
153 }
154 case "editName": {
155 await interaction.editReply({
156 embeds: [new EmojiEmbed()
157 .setTitle("Stats Channel")
158 .setDescription("Modal opened. If you can't see it, click back and try again.")
159 .setStatus("Success")
160 .setEmoji("SETTINGS.STATS.GREEN")
161 ],
162 components: [
163 new ActionRowBuilder<ButtonBuilder>().addComponents(
164 new ButtonBuilder()
165 .setLabel("Back")
166 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
167 .setStyle(ButtonStyle.Primary)
168 .setCustomId("back")
169 )
170 ]
171 });
172 showModal(i, {name: newChannelName, enabled: newChannelEnabled})
173
174 const out: Discord.ModalSubmitInteraction | ButtonInteraction| null = await modalInteractionCollector(m, interaction.user);
175 if (!out) continue;
176 if (out.isButton()) continue;
177 newChannelName = out.fields.getTextInputValue("text");
178 break;
179 }
180 case "toggleEnabled": {
181 await i.deferUpdate();
182 newChannelEnabled = !newChannelEnabled;
183 break;
184 }
185 }
186 } else {
187 await i.deferUpdate();
188 if (i.customId === "channel") {
189 newChannel = i.values[0];
190 }
191 }
192 } while (!closed)
193 if (cancelled) return originalObject;
194 if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject;
195 return currentObject;
196}
197const callback = async (interaction: CommandInteraction) => {
198 if (!interaction.guild) return;
199 const { renderChannel } = client.logger;
200 const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
201 let page = 0;
202 let closed = false;
203 const config = await client.database.guilds.read(interaction.guild.id);
204 let currentObject: ObjectSchema = config.stats;
205 let modified = false;
206 do {
207 const embed = new EmojiEmbed()
208 .setTitle("Stats Settings")
209 .setEmoji("SETTINGS.STATS.GREEN")
210 .setStatus("Success");
211 const noStatsChannels = Object.keys(currentObject).length === 0;
212 let current: { enabled: boolean; name: string; };
213
214 const pageSelect = new StringSelectMenuBuilder()
215 .setCustomId("page")
216 .setPlaceholder("Select a stats channel to manage");
217 const actionSelect = new StringSelectMenuBuilder()
218 .setCustomId("action")
219 .setPlaceholder("Perform an action")
220 .addOptions(
221 new StringSelectMenuOptionBuilder()
222 .setLabel("Edit")
223 .setDescription("Edit the stats channel")
224 .setValue("edit")
225 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
226 new StringSelectMenuOptionBuilder()
227 .setLabel("Delete")
228 .setDescription("Delete the stats channel")
229 .setValue("delete")
230 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
231 );
232 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
233 .addComponents(
234 new ButtonBuilder()
235 .setCustomId("back")
236 .setStyle(ButtonStyle.Primary)
237 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
238 .setDisabled(page === 0),
239 new ButtonBuilder()
240 .setCustomId("next")
241 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
242 .setStyle(ButtonStyle.Primary)
243 .setDisabled(page === Object.keys(currentObject).length - 1),
244 new ButtonBuilder()
245 .setCustomId("add")
246 .setLabel("Create new")
247 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
248 .setStyle(ButtonStyle.Secondary)
249 .setDisabled(Object.keys(currentObject).length >= 24),
250 new ButtonBuilder()
251 .setCustomId("save")
252 .setLabel("Save")
253 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
254 .setStyle(ButtonStyle.Success)
255 .setDisabled(modified),
256 );
257 if (noStatsChannels) {
258 embed.setDescription("No stats channels have been set up yet. Use the button below to add one.\n\n" +
259 createPageIndicator(1, 1, undefined, true)
260 );
261 pageSelect.setDisabled(true);
262 actionSelect.setDisabled(true);
263 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
264 .setLabel("No stats channels")
265 .setValue("none")
266 );
267 } else {
268 page = Math.min(page, Object.keys(currentObject).length - 1);
269 current = currentObject[Object.keys(config.stats)[page]!]!
270 actionSelect.addOptions(new StringSelectMenuOptionBuilder()
271 .setLabel(current.enabled ? "Disable" : "Enable")
272 .setValue("toggleEnabled")
273 .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
274 .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
275 );
276 embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` +
277 `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
278 `**Name:** \`${current.name}\`\n` +
279 `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' +
280 createPageIndicator(Object.keys(config.stats).length, page)
281 );
282 for (const [id, { name, enabled }] of Object.entries(currentObject)) {
283 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
284 .setLabel(`${name} (${renderChannel(id)})`)
285 .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
286 .setDescription(`${enabled ? "Enabled" : "Disabled"}`)
287 .setValue(id)
288 );
289 }
290 }
291
292 interaction.editReply({embeds: [embed], components: [
293 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
294 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
295 buttonRow
296 ]});
297
298 let i: StringSelectMenuInteraction | ButtonInteraction;
299 try {
300 i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction;
301 } catch (e) {
302 closed = true;
Skyler Greyad002172022-08-16 18:48:26 +0100303 continue;
Skyler Grey75ea9172022-08-06 10:22:23 +0100304 }
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500305
306 if(i.isStringSelectMenu()) {
307 switch(i.customId) {
308 case "page": {
309 await i.deferUpdate();
310 page = Object.keys(currentObject).indexOf(i.values[0]!);
311 break;
312 }
313 case "action": {
314 modified = true;
315 switch(i.values[0]!) {
316 case "edit": {
317 showModal(i, current!)
318 await interaction.editReply({
319 embeds: [
320 new EmojiEmbed()
321 .setTitle("Stats Channel")
322 .setDescription("Modal opened. If you can't see it, click back and try again.")
323 .setStatus("Success")
324 .setEmoji("SETTINGS.STATS.GREEN")
325 ],
326 components: [
327 new ActionRowBuilder<ButtonBuilder>().addComponents(
328 new ButtonBuilder()
329 .setLabel("Back")
330 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
331 .setStyle(ButtonStyle.Primary)
332 .setCustomId("back")
333 )
334 ]
335 });
336 let out: Discord.ModalSubmitInteraction | null;
337 try {
338 out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
339 } catch (e) {
340 continue;
341 }
342 if (!out) continue
343 if (out.isButton()) continue;
344 currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text");
345 break;
346 }
347 case "toggleEnabled": {
348 await i.deferUpdate();
349 currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled;
350 modified = true;
351 break;
352 }
353 case "delete": {
354 await i.deferUpdate();
355 currentObject = Object.fromEntries(Object.entries(currentObject).filter(([k]) => k !== Object.keys(currentObject)[page]!));
356 page = Math.min(page, Object.keys(currentObject).length - 1);
357 modified = true;
358 break;
359 }
360 }
361 break;
362 }
363 }
364 } else {
365 await i.deferUpdate();
366 switch(i.customId) {
367 case "back": {
368 page--;
369 break;
370 }
371 case "next": {
372 page++;
373 break;
374 }
375 case "add": {
376 currentObject = await addStatsChannel(interaction, m, currentObject);
377 page = Object.keys(currentObject).length - 1;
378 break;
379 }
380 case "save": {
381 client.database.guilds.write(interaction.guild.id, {stats: currentObject});
382 singleNotify("statsChannelDeleted", interaction.guild.id, true);
383 modified = false;
384 break;
385 }
386 }
pineafan0bc04162022-07-25 17:22:26 +0100387 }
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500388
389 } while (!closed);
390 await interaction.deleteReply()
pineafan63fc5e22022-08-04 22:04:10 +0100391};
pineafan708692b2022-07-24 22:16:22 +0100392
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500393const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100394 const member = interaction.member as Discord.GuildMember;
TheCodedProfafca98b2023-01-17 22:25:43 -0500395 if (!member.permissions.has("ManageChannels"))
PineaFan0d06edc2023-01-17 22:10:31 +0000396 return "You must have the *Manage Channels* permission to use this command";
pineafan708692b2022-07-24 22:16:22 +0100397 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100398};
pineafan708692b2022-07-24 22:16:22 +0100399
PineaFan538d3752023-01-12 21:48:23 +0000400
pineafan708692b2022-07-24 22:16:22 +0100401export { command };
402export { callback };
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500403export { check };