blob: 354a948068a308a161d24950eb413e257a3e853a [file] [log] [blame]
TheCodedProf4f79da12023-01-31 16:50:37 -05001import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, CommandInteraction, GuildMember, Message, ModalBuilder, ModalSubmitInteraction, PermissionsBitField, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
TheCodedProfa112f612023-01-28 18:06:45 -05002import client from "../../utils/client.js";
TheCodedProfb5e9d552023-01-29 15:43:26 -05003import createPageIndicator, { createVerticalTrack } from "../../utils/createPageIndicator.js";
4import { LoadingEmbed } from "../../utils/defaults.js";
5import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
6import getEmojiByName from "../../utils/getEmojiByName.js";
7import ellipsis from "../../utils/ellipsis.js";
8import { modalInteractionCollector } from "../../utils/dualCollector.js";
TheCodedProfa112f612023-01-28 18:06:45 -05009
TheCodedProf4f79da12023-01-31 16:50:37 -050010const { renderRole } = client.logger
11
TheCodedProfa112f612023-01-28 18:06:45 -050012const command = (builder: SlashCommandSubcommandBuilder) =>
13 builder
14 .setName("tracks")
15 .setDescription("Manage the tracks for the server")
16
TheCodedProfb5e9d552023-01-29 15:43:26 -050017interface ObjectSchema {
18 name: string;
19 retainPrevious: boolean;
20 nullable: boolean;
21 track: string[];
22 manageableBy: string[];
23}
24
TheCodedProf4f79da12023-01-31 16:50:37 -050025
TheCodedProfb5e9d552023-01-29 15:43:26 -050026const editName = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, current?: string) => {
27
28 let name = current ?? "";
29 const modal = new ModalBuilder()
30 .setTitle("Edit Name and Description")
31 .setCustomId("editNameDescription")
32 .addComponents(
33 new ActionRowBuilder<TextInputBuilder>()
34 .addComponents(
35 new TextInputBuilder()
36 .setLabel("Name")
37 .setCustomId("name")
38 .setPlaceholder("Name here...") // TODO: Make better placeholder
39 .setStyle(TextInputStyle.Short)
40 .setValue(name ?? "")
41 .setRequired(true)
42 )
43 )
44 const button = new ActionRowBuilder<ButtonBuilder>()
45 .addComponents(
46 new ButtonBuilder()
47 .setCustomId("back")
48 .setLabel("Back")
49 .setStyle(ButtonStyle.Secondary)
50 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
51 )
52
53 await i.showModal(modal)
54 await interaction.editReply({
55 embeds: [
56 new EmojiEmbed()
57 .setTitle("Tracks")
58 .setDescription("Modal opened. If you can't see it, click back and try again.")
59 .setStatus("Success")
60 ],
61 components: [button]
62 });
63
64 let out: ModalSubmitInteraction | null;
65 try {
66 out = await modalInteractionCollector(
67 m,
68 (m) => m.channel!.id === interaction.channel!.id,
69 (_) => true
70 ) as ModalSubmitInteraction | null;
71 } catch (e) {
72 console.error(e);
73 out = null;
74 }
75 if(!out) return name;
76 if (out.isButton()) return name;
77 if(!out.fields) return name;
78 name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
79 return name
80
81}
82
83const reorderTracks = async (interaction: ButtonInteraction, m: Message, roles: Collection<string, Role>, currentObj: string[]) => {
84 let reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
85 .addComponents(
86 new StringSelectMenuBuilder()
87 .setCustomId("reorder")
88 .setPlaceholder("Select all roles in the order you want users to gain them (Lowest to highest rank).")
89 .setMinValues(currentObj.length)
90 .setMaxValues(currentObj.length)
91 .addOptions(
92 currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
93 .setLabel(roles.get(o)!.name)
94 .setValue(i.toString())
95 )
96 )
97 );
98 let buttonRow = new ActionRowBuilder<ButtonBuilder>()
99 .addComponents(
100 new ButtonBuilder()
101 .setCustomId("back")
102 .setLabel("Back")
103 .setStyle(ButtonStyle.Secondary)
104 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
105 )
106 await interaction.editReply({
107 embeds: [
108 new EmojiEmbed()
109 .setTitle("Tracks")
110 .setDescription("Select all roles in the order you want users to gain them (Lowest to highest rank).")
111 .setStatus("Success")
112 ],
113 components: [reorderRow, buttonRow]
114 });
115 let out: StringSelectMenuInteraction | ButtonInteraction | null;
116 try {
117 out = await m.awaitMessageComponent({
118 filter: (i) => i.channel!.id === interaction.channel!.id,
119 time: 300000
120 }) as StringSelectMenuInteraction | ButtonInteraction | null;
121 } catch (e) {
122 console.error(e);
123 out = null;
124 }
125 if(!out) return;
126 out.deferUpdate();
127 if (out.isButton()) return;
128 if(!out.values) return;
129 const values = out.values;
130
131 const newOrder: string[] = currentObj.map((_, i) => {
132 const index = values.findIndex(v => v === i.toString());
133 return currentObj[index];
134 }) as string[];
135
136 return newOrder;
137}
138
139const editTrack = async (interaction: ButtonInteraction | StringSelectMenuInteraction, message: Message, roles: Collection<string, Role>, current?: ObjectSchema) => {
TheCodedProf4f79da12023-01-31 16:50:37 -0500140 const isAdmin = (interaction.member!.permissions as PermissionsBitField).has("Administrator");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500141 if(!current) {
142 current = {
143 name: "",
144 retainPrevious: false,
145 nullable: false,
146 track: [],
147 manageableBy: []
148 }
149 }
TheCodedProf4f79da12023-01-31 16:50:37 -0500150
TheCodedProfb5e9d552023-01-29 15:43:26 -0500151 const roleSelect = new ActionRowBuilder<RoleSelectMenuBuilder>()
152 .addComponents(
153 new RoleSelectMenuBuilder()
154 .setCustomId("addRole")
155 .setPlaceholder("Select a role to add")
TheCodedProf4f79da12023-01-31 16:50:37 -0500156 .setDisabled(!isAdmin)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500157 );
158 let closed = false;
159 do {
160 const editableRoles: string[] = current.track.map((r) => {
TheCodedProf4f79da12023-01-31 16:50:37 -0500161 if(!(roles.get(r)!.position >= (interaction.member as GuildMember).roles.highest.position)) return roles.get(r)!.name;
TheCodedProfb5e9d552023-01-29 15:43:26 -0500162 }).filter(v => v !== undefined) as string[];
163 const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
164 .addComponents(
165 new StringSelectMenuBuilder()
166 .setCustomId("removeRole")
167 .setPlaceholder("Select a role to remove")
TheCodedProf4f79da12023-01-31 16:50:37 -0500168 .setDisabled(!isAdmin)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500169 .addOptions(
170 editableRoles.map((r, i) => {
171 return new StringSelectMenuOptionBuilder()
172 .setLabel(r)
173 .setValue(i.toString())}
174 )
175 )
176 );
TheCodedProf4f79da12023-01-31 16:50:37 -0500177 const buttons = new ActionRowBuilder<ButtonBuilder>()
178 .addComponents(
179 new ButtonBuilder()
180 .setCustomId("back")
181 .setLabel("Back")
182 .setStyle(ButtonStyle.Secondary)
183 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
184 new ButtonBuilder()
185 .setCustomId("edit")
186 .setLabel("Edit Name")
187 .setStyle(ButtonStyle.Primary)
188 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
189 new ButtonBuilder()
190 .setCustomId("reorder")
191 .setLabel("Reorder")
192 .setDisabled(!isAdmin)
193 .setStyle(ButtonStyle.Primary)
194 .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji),
195 new ButtonBuilder()
196 .setCustomId("retainPrevious")
197 .setLabel("Retain Previous")
198 .setStyle(current.retainPrevious ? ButtonStyle.Success : ButtonStyle.Danger)
199 .setEmoji(getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji),
200 new ButtonBuilder()
201 .setCustomId("nullable")
202 .setLabel(`Role ${current.nullable ? "Not " : ""}Required`)
203 .setStyle(current.nullable ? ButtonStyle.Success : ButtonStyle.Danger)
204 .setEmoji(getEmojiByName("CONTROL." + (current.nullable ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji)
205 );
206
TheCodedProfb5e9d552023-01-29 15:43:26 -0500207 let allowed: boolean[] = [];
208 for (const role of current.track) {
209 const disabled: boolean =
210 roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position;
211 allowed.push(disabled)
212 }
TheCodedProf4f79da12023-01-31 16:50:37 -0500213 const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500214
215 const embed = new EmojiEmbed()
216 .setTitle("Tracks")
217 .setDescription(
218 `**Currently Editing:** ${current.name}\n\n` +
TheCodedProf4f79da12023-01-31 16:50:37 -0500219 `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` +
220 `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` +
221 createVerticalTrack(
222 mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false), allowed)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500223 )
TheCodedProf4f79da12023-01-31 16:50:37 -0500224 .setStatus("Success")
TheCodedProfb5e9d552023-01-29 15:43:26 -0500225
TheCodedProf4f79da12023-01-31 16:50:37 -0500226 interaction.editReply({embeds: [embed], components: [roleSelect, selectMenu, buttons]});
TheCodedProfb5e9d552023-01-29 15:43:26 -0500227
228 let out: ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null;
229
230 try {
231 out = await message.awaitMessageComponent({
232 filter: (i) => i.channel!.id === interaction.channel!.id,
233 time: 300000
234 }) as ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null;
235 } catch (e) {
236 console.error(e);
237 out = null;
238 }
239
240 if(!out) return;
241 if (out.isButton()) {
242 out.deferUpdate();
243 switch(out.customId) {
244 case "back":
245 closed = true;
246 break;
247 case "edit":
248 current.name = (await editName(out, interaction, message, current.name))!;
249 break;
250 case "reorder":
251 current.track = (await reorderTracks(out, message, roles, current.track))!;
TheCodedProf4f79da12023-01-31 16:50:37 -0500252 break;
253 case "retainPrevious":
254 current.retainPrevious = !current.retainPrevious;
255 break;
256 case "nullable":
257 current.nullable = !current.nullable;
258 break;
TheCodedProfb5e9d552023-01-29 15:43:26 -0500259 }
260 } else if (out.isStringSelectMenu()) {
261 out.deferUpdate();
262 switch(out.customId) {
263 case "removeRole":
264 const index = current.track.findIndex(v => v === editableRoles[parseInt((out! as StringSelectMenuInteraction).values![0]!)]);
265 current.track.splice(index, 1);
266 break;
267 }
268 } else {
269 switch(out.customId) {
270 case "addRole":
271 const role = out.values![0]!;
272 if(!current.track.includes(role)) {
273 current.track.push(role);
274 }
275 out.reply({content: "That role is already on this track", ephemeral: true})
276 break;
277 }
278 }
279
280 } while(!closed);
281 return current;
282}
TheCodedProfa112f612023-01-28 18:06:45 -0500283
284const callback = async (interaction: CommandInteraction) => {
285
TheCodedProfb5e9d552023-01-29 15:43:26 -0500286 const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true})
287 const config = await client.database.guilds.read(interaction.guild!.id);
288 const tracks: ObjectSchema[] = config.tracks;
289 const roles = await interaction.guild!.roles.fetch();
TheCodedProfb5e9d552023-01-29 15:43:26 -0500290
291 let page = 0;
292 let closed = false;
293 let modified = false;
294
295 do {
296 const embed = new EmojiEmbed()
297 .setTitle("Track Settings")
298 .setEmoji("TRACKS.ICON")
299 .setStatus("Success");
300 const noTracks = config.tracks.length === 0;
301 let current: ObjectSchema;
302
303 const pageSelect = new StringSelectMenuBuilder()
304 .setCustomId("page")
305 .setPlaceholder("Select a track to manage");
306 const actionSelect = new StringSelectMenuBuilder()
307 .setCustomId("action")
308 .setPlaceholder("Perform an action")
309 .addOptions(
310 new StringSelectMenuOptionBuilder()
311 .setLabel("Edit")
312 .setDescription("Edit this track")
313 .setValue("edit")
314 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
315 new StringSelectMenuOptionBuilder()
316 .setLabel("Delete")
317 .setDescription("Delete this track")
318 .setValue("delete")
319 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
320 );
321 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
322 .addComponents(
323 new ButtonBuilder()
324 .setCustomId("back")
325 .setStyle(ButtonStyle.Primary)
326 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
327 .setDisabled(page === 0),
328 new ButtonBuilder()
329 .setCustomId("next")
330 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
331 .setStyle(ButtonStyle.Primary)
332 .setDisabled(page === Object.keys(tracks).length - 1),
333 new ButtonBuilder()
334 .setCustomId("add")
335 .setLabel("New Track")
336 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
337 .setStyle(ButtonStyle.Secondary)
338 .setDisabled(Object.keys(tracks).length >= 24),
339 new ButtonBuilder()
340 .setCustomId("save")
341 .setLabel("Save")
342 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
343 .setStyle(ButtonStyle.Success)
344 .setDisabled(!modified),
345 );
346 if(noTracks) {
347 embed.setDescription("No tracks have been set up yet. Use the button below to add one.\n\n" +
348 createPageIndicator(1, 1, undefined, true)
349 );
350 pageSelect.setDisabled(true);
351 actionSelect.setDisabled(true);
352 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
353 .setLabel("No tracks")
354 .setValue("none")
355 );
356 } else {
357 page = Math.min(page, Object.keys(tracks).length - 1);
358 current = tracks[page]!;
TheCodedProf4f79da12023-01-31 16:50:37 -0500359 const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500360 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
361 `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` +
362 `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` +
TheCodedProf4f79da12023-01-31 16:50:37 -0500363 createVerticalTrack(mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false)) +
TheCodedProfb5e9d552023-01-29 15:43:26 -0500364 `\n${createPageIndicator(config.tracks.length, page)}`
365 );
366
367 pageSelect.addOptions(
368 tracks.map((key: ObjectSchema, index) => {
369 return new StringSelectMenuOptionBuilder()
370 .setLabel(ellipsis(key.name, 50))
371 .setDescription(ellipsis(roles.get(key.track[0]!)?.name!, 50))
372 .setValue(index.toString());
373 })
374 );
375
376 }
377
378 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
379 let i: StringSelectMenuInteraction | ButtonInteraction;
380 try {
381 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;
382 } catch (e) {
383 closed = true;
384 break;
385 }
386
387 await i.deferUpdate();
388 if (i.isButton()) {
389 switch (i.customId) {
390 case "back":
391 page--;
392 break;
393 case "next":
394 page++;
395 break;
396 case "add":
397 let newPage = await editTrack(i, m, roles)
398 if(!newPage) break;
399 tracks.push();
400 page = tracks.length - 1;
401 break;
402 case "save":
TheCodedProf4f79da12023-01-31 16:50:37 -0500403 client.database.guilds.write(interaction.guild!.id, {tracks: tracks});
TheCodedProfb5e9d552023-01-29 15:43:26 -0500404 modified = false;
405 break;
406 }
407 } else if (i.isStringSelectMenu()) {
408 switch (i.customId) {
409 case "action":
410 switch(i.values[0]) {
411 case "edit":
412 let edited = await editTrack(i, m, roles, current!);
413 if(!edited) break;
414 tracks[page] = edited;
415 modified = true;
416 break;
417 case "delete":
418 if(page === 0 && tracks.keys.length - 1 > 0) page++;
419 else page--;
420 tracks.splice(page, 1);
421 break;
422 }
423 break;
424 case "page":
425 page = parseInt(i.values[0]!);
426 break;
427 }
428 }
429
430 } while (!closed)
TheCodedProfa112f612023-01-28 18:06:45 -0500431
432}
433
434const check = (interaction: CommandInteraction, _partial: boolean = false) => {
435 const member = interaction.member as GuildMember;
436 if (!member.permissions.has("ManageRoles"))
437 return "You must have the *Manage Server* permission to use this command";
438 return true;
439};
440
441export { command };
442export { callback };
443export { check };