blob: 7dd9cf4fe074e9142a085baf3b994a8aa2fae315 [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
76/*
77let out: Discord.ModalSubmitInteraction | null = null;
78try {
79 out = await modalInteractionCollector(
80 m,
81 (m) => m.channel!.id === interaction.channel!.id,
82 (_) => true
83 ) as Discord.ModalSubmitInteraction | null;
84} catch (e) {
85 continue;
86}
87if (!out) continue
88out = out!;
89if (!out.fields) continue;
90if (out.isButton()) continue;
91const name = out.fields.getTextInputValue("text")
92*/
93
94const addStatsChannel = async (interaction: CommandInteraction, m: Message, currentObject: ObjectSchema): Promise<ObjectSchema> => {
95 let closed = false;
96 let cancelled = false;
97 const originalObject = Object.fromEntries(Object.entries(currentObject).map(([k, v]) => [k, {...v}]));
98 let newChannel: string | undefined;
99 let newChannelName: string = "{memberCount:all}-members";
100 let newChannelEnabled: boolean = true;
101 do {
102 await interaction.editReply({
103 embeds: [new EmojiEmbed()
104 .setTitle("Stats Channel")
105 .setDescription(
106 `New stats channel` + (newChannel ? ` in <#${newChannel}>` : "") + "\n\n" +
107 `**Name:** \`${newChannelName}\`\n` +
108 `**Preview:** ${await convertCurlyBracketString(newChannelName, interaction.user!.id, interaction.user.username, interaction.guild!.name, interaction.guild!.members)}\n` +
109 `**Enabled:** ${newChannelEnabled ? "Yes" : "No"}\n\n`
110 )
111 .setEmoji("SETTINGS.STATS.GREEN")
112 .setStatus("Success")
113 ], components: [
114 new ActionRowBuilder<ChannelSelectMenuBuilder>().addComponents(
115 new ChannelSelectMenuBuilder()
116 .setCustomId("channel")
117 ),
118 new ActionRowBuilder<ButtonBuilder>().addComponents(
119 new ButtonBuilder()
120 .setLabel("Cancel")
121 .setEmoji(getEmojiByName("CONTROL.CROSS", "id"))
122 .setStyle(ButtonStyle.Danger)
123 .setCustomId("back"),
124 new ButtonBuilder()
125 .setLabel("Save")
126 .setEmoji(getEmojiByName("ICONS.SAVE", "id"))
127 .setStyle(ButtonStyle.Success)
128 .setCustomId("save"),
129 new ButtonBuilder()
130 .setLabel("Edit name")
131 .setEmoji(getEmojiByName("ICONS.EDIT", "id"))
132 .setStyle(ButtonStyle.Primary)
133 .setCustomId("editName"),
134 new ButtonBuilder()
135 .setLabel(newChannelEnabled ? "Enabled" : "Disabled")
136 .setEmoji(getEmojiByName(newChannelEnabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id"))
137 .setStyle(ButtonStyle.Secondary)
138 .setCustomId("toggleEnabled")
139 )
140 ]
141 });
142 let i: ButtonInteraction | ChannelSelectMenuInteraction;
143 try {
144 i = await m.awaitMessageComponent({ time: 300000, filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id }}) as ButtonInteraction | ChannelSelectMenuInteraction;
145 } catch (e) {
146 closed = true;
147 cancelled = true;
148 break;
149 }
150 if (i.isButton()) {
151 switch (i.customId) {
152 case "back":
153 if(!i.deferred) await i.deferUpdate();
154 closed = true;
155 break;
156 case "save":
157 if(!i.deferred) await i.deferUpdate(); //I'm lost...
158 if (newChannel) {
159 currentObject[newChannel] = {
160 name: newChannelName,
161 enabled: newChannelEnabled
162 }
163 }
164 closed = true;
165 break;
166 case "editName":
167 await interaction.editReply({
168 embeds: [new EmojiEmbed()
169 .setTitle("Stats Channel")
170 .setDescription("Modal opened. If you can't see it, click back and try again.")
171 .setStatus("Success")
172 .setEmoji("SETTINGS.STATS.GREEN")
173 ],
174 components: [
175 new ActionRowBuilder<ButtonBuilder>().addComponents(
176 new ButtonBuilder()
177 .setLabel("Back")
178 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
179 .setStyle(ButtonStyle.Primary)
180 .setCustomId("back")
181 )
182 ]
183 });
184 showModal(i, {name: newChannelName, enabled: newChannelEnabled})
185
186 const out = await modalInteractionCollector(
187 m,
188 (m) => m.channel!.id === interaction.channel!.id,
189 (_) => true
190 ) as Discord.ModalSubmitInteraction | null;
191 if (!out) continue;
192 if (!out.fields) continue;
193 if (out.isButton()) continue;
194 newChannelName = out.fields.getTextInputValue("text");
195 break;
196 case "toggleEnabled":
197 if(!i.deferred) await i.deferUpdate();
198 newChannelEnabled = !newChannelEnabled;
199 break;
200 }
201 } else {
202 if(!i.deferred) await i.deferUpdate();
203 if (i.customId === "channel") {
204 newChannel = i.values[0];
205 }
206 }
207 } while (!closed)
208 if (cancelled) return originalObject;
209 if (!(newChannel && newChannelName && newChannelEnabled)) return originalObject;
210 return currentObject; // check 157
211}
PineaFan5d98a4b2023-01-19 16:15:47 +0000212const callback = async (interaction: CommandInteraction) => {
213 if (!interaction.guild) return;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500214 const { renderChannel } = client.logger;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500215 const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500216 let page = 0;
217 let closed = false;
218 const config = await client.database.guilds.read(interaction.guild.id);
TheCodedProf9bc84752023-01-21 05:19:57 -0500219 let currentObject: ObjectSchema = config.stats;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500220 let modified = false;
PineaFan5d98a4b2023-01-19 16:15:47 +0000221 do {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500222 let embed = new EmojiEmbed()
223 .setTitle("Stats Settings")
224 .setEmoji("SETTINGS.STATS.GREEN")
225 .setStatus("Success");
226 const noStatsChannels = Object.keys(currentObject).length === 0;
227 let current: { enabled: boolean; name: string; };
228
229 const pageSelect = new StringSelectMenuBuilder()
PineaFan5d98a4b2023-01-19 16:15:47 +0000230 .setCustomId("page")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500231 .setPlaceholder("Select a stats channel to manage");
232 const actionSelect = new StringSelectMenuBuilder()
TheCodedProf4a6d5712023-01-19 15:54:40 -0500233 .setCustomId("action")
234 .setPlaceholder("Perform an action")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500235 .addOptions(
236 new StringSelectMenuOptionBuilder()
237 .setLabel("Edit")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500238 .setDescription("Edit the stats channel")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500239 .setValue("edit")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500240 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
241 new StringSelectMenuOptionBuilder()
242 .setLabel("Delete")
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500243 .setDescription("Delete the stats channel")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500244 .setValue("delete")
TheCodedProf4a6d5712023-01-19 15:54:40 -0500245 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500246 );
247 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
248 .addComponents(
249 new ButtonBuilder()
250 .setCustomId("back")
251 .setStyle(ButtonStyle.Primary)
252 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
253 .setDisabled(page === 0),
254 new ButtonBuilder()
255 .setCustomId("next")
256 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
257 .setStyle(ButtonStyle.Primary)
258 .setDisabled(page === Object.keys(currentObject).length - 1),
259 new ButtonBuilder()
260 .setCustomId("add")
261 .setLabel("Create new")
262 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
263 .setStyle(ButtonStyle.Secondary)
264 .setDisabled(Object.keys(currentObject).length >= 24),
265 new ButtonBuilder()
266 .setCustomId("save")
267 .setLabel("Save")
268 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
269 .setStyle(ButtonStyle.Success)
270 .setDisabled(modified),
TheCodedProf4a6d5712023-01-19 15:54:40 -0500271 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500272 if (noStatsChannels) {
273 embed.setDescription("No stats channels have been set up yet. Use the button below to add one.\n\n" +
274 createPageIndicator(1, 1, undefined, true)
275 );
276 pageSelect.setDisabled(true);
277 actionSelect.setDisabled(true);
278 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
279 .setLabel("No stats channels")
280 .setValue("none")
281 );
TheCodedProf4a6d5712023-01-19 15:54:40 -0500282 } else {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500283 page = Math.min(page, Object.keys(currentObject).length - 1);
284 current = currentObject[Object.keys(config.stats)[page]!]!
TheCodedProf4a6d5712023-01-19 15:54:40 -0500285 actionSelect.addOptions(new StringSelectMenuOptionBuilder()
286 .setLabel(current.enabled ? "Disable" : "Enable")
287 .setValue("toggleEnabled")
288 .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
289 .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
290 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500291
292 embed.setDescription(`**Currently Editing:** ${renderChannel(Object.keys(currentObject)[page]!)}\n\n` +
TheCodedProf4a6d5712023-01-19 15:54:40 -0500293 `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
294 `**Name:** \`${current.name}\`\n` +
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500295 `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}` + '\n\n' +
296 createPageIndicator(Object.keys(config.stats).length, page)
TheCodedProf4a6d5712023-01-19 15:54:40 -0500297 );
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500298 for (const [id, { name, enabled }] of Object.entries(currentObject)) {
299 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
300 .setLabel(`${name} (${renderChannel(id)})`)
301 .setEmoji(getEmojiByName(enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
302 .setDescription(`${enabled ? "Enabled" : "Disabled"}`)
303 .setValue(id)
304 );
305 }
306 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500307
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500308 interaction.editReply({embeds: [embed], components: [
309 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
310 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
311 buttonRow
312 ]});
TheCodedProf4a6d5712023-01-19 15:54:40 -0500313
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500314 let i: StringSelectMenuInteraction | ButtonInteraction;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500315 try {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500316 i = await m.awaitMessageComponent({ filter: (interaction) => interaction.user.id === interaction.user.id, time: 60000 }) as StringSelectMenuInteraction | ButtonInteraction;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500317 } catch (e) {
318 closed = true;
319 continue;
320 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500321
322 if(i.isStringSelectMenu()) {
TheCodedProf4a6d5712023-01-19 15:54:40 -0500323 switch(i.customId) {
324 case "page":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500325 page = Object.keys(currentObject).indexOf(i.values[0]!);
TheCodedProf4a6d5712023-01-19 15:54:40 -0500326 break;
327 case "action":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500328 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500329 switch(i.values[0]!) {
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500330 case "edit": {
331 showModal(i, current!)
TheCodedProf4a6d5712023-01-19 15:54:40 -0500332 await interaction.editReply({
333 embeds: [
334 new EmojiEmbed()
335 .setTitle("Stats Channel")
336 .setDescription("Modal opened. If you can't see it, click back and try again.")
337 .setStatus("Success")
338 .setEmoji("SETTINGS.STATS.GREEN")
339 ],
340 components: [
341 new ActionRowBuilder<ButtonBuilder>().addComponents(
342 new ButtonBuilder()
343 .setLabel("Back")
344 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
345 .setStyle(ButtonStyle.Primary)
346 .setCustomId("back")
347 )
348 ]
349 });
TheCodedProf9bc84752023-01-21 05:19:57 -0500350 let out: Discord.ModalSubmitInteraction | null;
351 try {
352 out = await modalInteractionCollector(
353 m,
354 (m) => m.channel!.id === interaction.channel!.id,
355 (_) => true
356 ) as Discord.ModalSubmitInteraction | null;
357 } catch (e) {
358 continue;
359 }
360 if (!out) continue
361 if (!out.fields) continue
362 if (out.isButton()) continue;
363 currentObject[Object.keys(currentObject)[page]!]!.name = out.fields.getTextInputValue("text");
TheCodedProf4a6d5712023-01-19 15:54:40 -0500364 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500365 }
366 case "toggleEnabled": {
TheCodedProf4a6d5712023-01-19 15:54:40 -0500367 i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500368 currentObject[Object.keys(currentObject)[page]!]!.enabled = !currentObject[Object.keys(currentObject)[page]!]!.enabled;
369 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500370 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500371 }
372 case "delete": {
TheCodedProf4a6d5712023-01-19 15:54:40 -0500373 i.deferUpdate();
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500374 delete currentObject[Object.keys(currentObject)[page]!];
375 modified = true;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500376 break;
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500377 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500378 }
379 break;
380 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500381 } else {
TheCodedProf4a6d5712023-01-19 15:54:40 -0500382 i.deferUpdate();
383 switch(i.customId) {
384 case "back":
385 page--;
386 break;
387 case "next":
388 page++;
389 break;
390 case "add":
TheCodedProf9bc84752023-01-21 05:19:57 -0500391 currentObject = await addStatsChannel(interaction, m, currentObject);
TheCodedProf4a6d5712023-01-19 15:54:40 -0500392 break;
393 case "save":
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500394 client.database.guilds.write(interaction.guild.id, {stats: currentObject});
395 singleNotify("statsChannelDeleted", interaction.guild.id, true);
396 modified = false;
397 break;
TheCodedProf4a6d5712023-01-19 15:54:40 -0500398 }
399 }
TheCodedProfc2acbcc2023-01-20 17:23:51 -0500400
PineaFan5d98a4b2023-01-19 16:15:47 +0000401 } while (!closed);
pineafan63fc5e22022-08-04 22:04:10 +0100402};
pineafan708692b2022-07-24 22:16:22 +0100403
PineaFan64486c42022-12-28 09:21:04 +0000404const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100405 const member = interaction.member as Discord.GuildMember;
TheCodedProfafca98b2023-01-17 22:25:43 -0500406 if (!member.permissions.has("ManageChannels"))
PineaFan0d06edc2023-01-17 22:10:31 +0000407 return "You must have the *Manage Channels* permission to use this command";
pineafan708692b2022-07-24 22:16:22 +0100408 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100409};
pineafan708692b2022-07-24 22:16:22 +0100410
PineaFan538d3752023-01-12 21:48:23 +0000411
pineafan708692b2022-07-24 22:16:22 +0100412export { command };
413export { callback };
PineaFan5d98a4b2023-01-19 16:15:47 +0000414export { check };