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