blob: df99510c9a99c2dfff2e8ae1f955ceac6077ba20 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { LoadingEmbed } from "../../utils/defaults.js";
TheCodedProf4a6d5712023-01-19 15:54:40 -05002import Discord, { CommandInteraction, Message, ActionRowBuilder, StringSelectMenuBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuOptionBuilder, APIMessageComponentEmoji, MessageComponentInteraction, TextInputBuilder } 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";
Skyler Grey75ea9172022-08-06 10:22:23 +01007import { callback as statsChannelAddCallback } from "../../reflex/statsChannelUpdate.js";
pineafan63fc5e22022-08-04 22:04:10 +01008import singleNotify from "../../utils/singleNotify.js";
TheCodedProf4a6d5712023-01-19 15:54:40 -05009import getEmojiByName from "../../utils/getEmojiByName.js";
10import createPageIndicator from "../../utils/createPageIndicator.js";
11import { modalInteractionCollector } from "../../utils/dualCollector.js";
12import type { GuildConfig } from "../../utils/database.js";
pineafan708692b2022-07-24 22:16:22 +010013
14const command = (builder: SlashCommandSubcommandBuilder) =>
15 builder
pineafan63fc5e22022-08-04 22:04:10 +010016 .setName("stats")
Skyler Grey11236ba2022-08-08 21:13:33 +010017 .setDescription("Controls channels which update when someone joins or leaves the server")
pineafan708692b2022-07-24 22:16:22 +010018
TheCodedProf4a6d5712023-01-19 15:54:40 -050019type ChangesType = Record<string, { name?: string; enabled?: boolean; }>
20
21const applyChanges = (baseObject: GuildConfig['stats'], changes: ChangesType): GuildConfig['stats'] => {
22 for (const [id, { name, enabled }] of Object.entries(changes)) {
23 if (!baseObject[id]) baseObject[id] = { name: "", enabled: false};
24 if (name) baseObject[id]!.name = name;
25 if (enabled) baseObject[id]!.enabled = enabled;
26 }
27 return baseObject;
28}
29
30
PineaFan5d98a4b2023-01-19 16:15:47 +000031const callback = async (interaction: CommandInteraction) => {
TheCodedProf4a6d5712023-01-19 15:54:40 -050032 try{
PineaFan5d98a4b2023-01-19 16:15:47 +000033 if (!interaction.guild) return;
TheCodedProf4a6d5712023-01-19 15:54:40 -050034 const { renderChannel } = client.logger;
PineaFan5d98a4b2023-01-19 16:15:47 +000035 let closed = false;
36 let page = 0;
TheCodedProf4a6d5712023-01-19 15:54:40 -050037 const m: Message = await interaction.reply({ embeds: LoadingEmbed, ephemeral: true, fetchReply: true });
38 let changes: ChangesType = {};
PineaFan5d98a4b2023-01-19 16:15:47 +000039 do {
40 const config = await client.database.guilds.read(interaction.guild.id);
TheCodedProf4a6d5712023-01-19 15:54:40 -050041 const stats = config.stats;
42 let currentID = "";
43 let current: {
44 name: string;
45 enabled: boolean;
46 } = {
47 name: "",
48 enabled: false
49 };
50 let description = "";
PineaFan5d98a4b2023-01-19 16:15:47 +000051 let pageSelect = new StringSelectMenuBuilder()
52 .setCustomId("page")
53 .setPlaceholder("Select a stats channel to manage")
TheCodedProf4a6d5712023-01-19 15:54:40 -050054 .setDisabled(Object.keys(stats).length === 0)
pineafan0bc04162022-07-25 17:22:26 +010055 .setMinValues(1)
PineaFan5d98a4b2023-01-19 16:15:47 +000056 .setMaxValues(1);
TheCodedProf4a6d5712023-01-19 15:54:40 -050057 let actionSelect = new StringSelectMenuBuilder()
58 .setCustomId("action")
59 .setPlaceholder("Perform an action")
60 .setMinValues(1)
61 .setMaxValues(1)
62 .setDisabled(Object.keys(stats).length === 0)
63 .addOptions(
64 new StringSelectMenuOptionBuilder()
65 .setLabel("Edit")
66 .setValue("edit")
67 .setDescription("Edit the name of this stats channel")
68 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
69 new StringSelectMenuOptionBuilder()
70 .setLabel("Delete")
71 .setValue("delete")
72 .setDescription("Delete this stats channel")
73 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
74 );
75 if (Object.keys(stats).length === 0) {
76 description = "You do not have any stats channels set up yet"
77 pageSelect.addOptions(new StringSelectMenuOptionBuilder().setLabel("No stats channels").setValue("none"))
78 } else {
79 currentID = Object.keys(stats)[page]!
80 current = stats[currentID]!;
81 current = applyChanges({ [currentID]: current }, changes)[currentID]!;
82 // Propogate pageSelect with list of stats channels
83 for (const [id, { name, enabled }] of Object.entries(stats)) {
84 pageSelect.addOptions(
85 new StringSelectMenuOptionBuilder()
86 .setLabel(name)
87 .setValue(id)
88 .setDescription(`Enabled: ${enabled}`)
89 );
90 }
91 actionSelect.addOptions(new StringSelectMenuOptionBuilder()
92 .setLabel(current.enabled ? "Disable" : "Enable")
93 .setValue("toggleEnabled")
94 .setDescription(`Currently ${current.enabled ? "Enabled" : "Disabled"}, click to ${current.enabled ? "disable" : "enable"} this channel`)
95 .setEmoji(getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS", "id") as APIMessageComponentEmoji)
96 );
97 description = `**Currently Editing:** ${renderChannel(currentID)}\n\n` +
98 `${getEmojiByName(current.enabled ? "CONTROL.TICK" : "CONTROL.CROSS")} Currently ${current.enabled ? "Enabled" : "Disabled"}\n` +
99 `**Name:** \`${current.name}\`\n` +
100 `**Preview:** ${await convertCurlyBracketString(current.name, interaction.user.id, interaction.user.username, interaction.guild.name, interaction.guild.members)}`
Skyler Grey75ea9172022-08-06 10:22:23 +0100101 }
TheCodedProf4a6d5712023-01-19 15:54:40 -0500102 const row = new ActionRowBuilder<ButtonBuilder>()
103 .addComponents(
104 new ButtonBuilder()
105 .setCustomId("back")
106 .setStyle(ButtonStyle.Primary)
107 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
108 .setDisabled(page === 0),
109 new ButtonBuilder()
110 .setCustomId("next")
111 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
112 .setStyle(ButtonStyle.Primary)
113 .setDisabled(page === Object.keys(stats).length - 1),
114 new ButtonBuilder()
115 .setCustomId("add")
116 .setLabel("Create new")
117 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
118 .setStyle(ButtonStyle.Secondary)
119 .setDisabled(Object.keys(stats).length >= 24),
120 new ButtonBuilder()
121 .setCustomId("save")
122 .setLabel("Save")
123 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
124 .setStyle(ButtonStyle.Success)
125 .setDisabled(Object.keys(changes).length === 0),
126 );
127
128 let embed = new EmojiEmbed()
129 .setTitle("Stats Channels")
130 .setDescription(description + "\n\n" + createPageIndicator(Object.keys(stats).length, page))
131 .setEmoji("SETTINGS.STATS.GREEN")
132 .setStatus("Success")
133
134 interaction.editReply({
135 embeds: [embed],
136 components: [
137 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect),
138 new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect),
139 row
140 ]
141 });
142 let i: MessageComponentInteraction;
143 try {
144 i = await m.awaitMessageComponent({ filter: (i) => i.user.id === interaction.user.id, time: 30000 });
145 } catch (e) {
146 closed = true;
147 continue;
148 }
149 if (i.isStringSelectMenu()) {
150 switch(i.customId) {
151 case "page":
152 page = Object.keys(stats).indexOf(i.values[0]!);
153 i.deferUpdate();
154 break;
155 case "action":
156 if(!changes[currentID]) changes[currentID] = {};
157 switch(i.values[0]!) {
158 case "edit":
159 await i.showModal(
160 new Discord.ModalBuilder()
161 .setCustomId("modal")
162 .setTitle(`Stats channel name`)
163 .addComponents(
164 new ActionRowBuilder<TextInputBuilder>().addComponents(
165 new TextInputBuilder()
166 .setCustomId("ex1")
167 .setLabel("Server Info (1/3)")
168 .setPlaceholder(
169 `{serverName} - This server's name\n\n` +
170 `These placeholders will be replaced with the server's name, etc..`
171 )
172 .setMaxLength(1)
173 .setRequired(false)
174 .setStyle(Discord.TextInputStyle.Paragraph)
175 ),
176 new ActionRowBuilder<TextInputBuilder>().addComponents(
177 new TextInputBuilder()
178 .setCustomId("ex2")
179 .setLabel("Member Counts (2/3) - {MemberCount:...}")
180 .setPlaceholder(
181 `{:all} - Total member count\n` +
182 `{:humans} - Total non-bot users\n` +
183 `{:bots} - Number of bots\n`
184 )
185 .setMaxLength(1)
186 .setRequired(false)
187 .setStyle(Discord.TextInputStyle.Paragraph)
188 ),
189 new ActionRowBuilder<TextInputBuilder>().addComponents(
190 new TextInputBuilder()
191 .setCustomId("ex3")
192 .setLabel("Latest Member (3/3) - {member:...}")
193 .setPlaceholder(
194 `{:name} - The members name\n`
195 )
196 .setMaxLength(1)
197 .setRequired(false)
198 .setStyle(Discord.TextInputStyle.Paragraph)
199 ),
200 new ActionRowBuilder<TextInputBuilder>().addComponents(
201 new TextInputBuilder()
202 .setCustomId("text")
203 .setLabel("Channel name input")
204 .setMaxLength(1000)
205 .setRequired(true)
206 .setStyle(Discord.TextInputStyle.Short)
207 .setValue(current.name)
208 )
209 )
210 );
211 await interaction.editReply({
212 embeds: [
213 new EmojiEmbed()
214 .setTitle("Stats Channel")
215 .setDescription("Modal opened. If you can't see it, click back and try again.")
216 .setStatus("Success")
217 .setEmoji("SETTINGS.STATS.GREEN")
218 ],
219 components: [
220 new ActionRowBuilder<ButtonBuilder>().addComponents(
221 new ButtonBuilder()
222 .setLabel("Back")
223 .setEmoji(getEmojiByName("CONTROL.LEFT", "id"))
224 .setStyle(ButtonStyle.Primary)
225 .setCustomId("back")
226 )
227 ]
228 });
229 let out: Discord.ModalSubmitInteraction | null;
230 try {
231 out = await modalInteractionCollector(
232 m,
233 (m) => m.channel!.id === interaction.channel!.id,
234 (_) => true
235 ) as Discord.ModalSubmitInteraction | null;
236 } catch (e) {
237 continue;
238 }
239 if (!out) continue
240 if (!out.fields) continue
241 if (out.isButton()) continue;
242 const newString = out.fields.getTextInputValue("text");
243 if (!newString) continue;
244 changes[currentID]!.name = newString;
245 break;
246 case "delete":
247 changes[currentID] = {};
248 i.deferUpdate();
249 break;
250 case "toggleEnabled":
251 changes[currentID]!.enabled = !stats[currentID]!.enabled;
252 i.deferUpdate();
253 break;
254 }
255 break;
256 }
257 } else if (i.isButton()) {
258 i.deferUpdate();
259 switch(i.customId) {
260 case "back":
261 page--;
262 break;
263 case "next":
264 page++;
265 break;
266 case "add":
267 break;
268 case "save":
269 let changed = applyChanges(config.stats, changes);
270 singleNotify("statsChannelDeleted", interaction.guild.id, true)
271 config.stats = changed;
272 changes = {}
273 await client.database.guilds.write(interaction.guildId!, config);
274 }
275 }
276 console.log(changes, config.stats);
PineaFan5d98a4b2023-01-19 16:15:47 +0000277 } while (!closed);
TheCodedProf4a6d5712023-01-19 15:54:40 -0500278 } catch(e) {
279 console.log(e)
280 }
pineafan63fc5e22022-08-04 22:04:10 +0100281};
pineafan708692b2022-07-24 22:16:22 +0100282
PineaFan64486c42022-12-28 09:21:04 +0000283const check = (interaction: CommandInteraction) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100284 const member = interaction.member as Discord.GuildMember;
TheCodedProfafca98b2023-01-17 22:25:43 -0500285 if (!member.permissions.has("ManageChannels"))
PineaFan0d06edc2023-01-17 22:10:31 +0000286 return "You must have the *Manage Channels* permission to use this command";
pineafan708692b2022-07-24 22:16:22 +0100287 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100288};
pineafan708692b2022-07-24 22:16:22 +0100289
PineaFan538d3752023-01-12 21:48:23 +0000290
pineafan708692b2022-07-24 22:16:22 +0100291export { command };
292export { callback };
PineaFan5d98a4b2023-01-19 16:15:47 +0000293export { check };