blob: 87968929736435d81e1c86759ab7e7c3219c97f3 [file] [log] [blame]
TheCodedProfafca98b2023-01-17 22:25:43 -05001import type Discord from "discord.js";
TheCodedProf1c3ad3c2023-01-25 17:58:36 -05002import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, Message, ModalBuilder, RoleSelectMenuBuilder, RoleSelectMenuInteraction, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
TheCodedProff86ba092023-01-27 17:10:07 -05003import type { SlashCommandSubcommandBuilder } from "discord.js";
TheCodedProf1c3ad3c2023-01-25 17:58:36 -05004import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
5import { LoadingEmbed } from "../../utils/defaults.js";
6import client from "../../utils/client.js";
7import getEmojiByName from "../../utils/getEmojiByName.js";
8import createPageIndicator from "../../utils/createPageIndicator.js";
9import { configToDropdown } from "../../actions/roleMenu.js";
TheCodedProff4facde2023-01-28 13:42:48 -050010import { modalInteractionCollector } from "../../utils/dualCollector.js";
11import lodash from 'lodash';
TheCodedProfa112f612023-01-28 18:06:45 -050012
TheCodedProff4facde2023-01-28 13:42:48 -050013const isEqual = lodash.isEqual;
TheCodedProfa112f612023-01-28 18:06:45 -050014
pineafanda6e5342022-07-03 10:03:16 +010015const command = (builder: SlashCommandSubcommandBuilder) =>
16 builder
pineafan63fc5e22022-08-04 22:04:10 +010017 .setName("rolemenu")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050018 .setDescription("rolemenu")
19
20interface ObjectSchema {
21 name: string;
22 description: string;
23 min: number;
24 max: number;
25 options: {
26 name: string;
27 description: string | null;
28 role: string;
29 }[];
30}
31
TheCodedProff4facde2023-01-28 13:42:48 -050032const defaultRolePageConfig = {
33 name: "Role Menu Page",
34 description: "A new role menu page",
35 min: 0,
36 max: 0,
37 options: [
38 {name: "Role 1", description: null, role: "No role set"}
39 ]
40}
41
TheCodedProfa112f612023-01-28 18:06:45 -050042const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, currentObj: ObjectSchema[]) => {
43 let reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
44 .addComponents(
45 new StringSelectMenuBuilder()
46 .setCustomId("reorder")
47 .setPlaceholder("Select a page to move...")
48 .setMinValues(1)
49 .addOptions(
50 currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
51 .setLabel(o.name)
52 .setValue(i.toString())
53 )
54 )
55 );
56 let buttonRow = new ActionRowBuilder<ButtonBuilder>()
57 .addComponents(
58 new ButtonBuilder()
59 .setCustomId("back")
60 .setLabel("Back")
61 .setStyle(ButtonStyle.Secondary)
62 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
63 )
64 await interaction.editReply({
65 embeds: [
66 new EmojiEmbed()
67 .setTitle("Role Menu")
68 .setDescription("Select pages in the order you want them to appear.")
69 .setStatus("Success")
70 ],
71 components: [reorderRow, buttonRow]
72 });
73 let out: StringSelectMenuInteraction | ButtonInteraction | null;
74 try {
75 out = await m.awaitMessageComponent({
76 filter: (i) => i.channel!.id === interaction.channel!.id,
77 time: 300000
78 }) as StringSelectMenuInteraction | ButtonInteraction | null;
79 } catch (e) {
80 console.error(e);
81 out = null;
82 }
83 if(!out) return;
84 if (out.isButton()) return;
85 if(!out.values) return;
86 const values = out.values;
87
88 const newOrder: ObjectSchema[] = currentObj.map((_, i) => {
89 const index = values.findIndex(v => v === i.toString());
90 return currentObj[index];
91 }) as ObjectSchema[];
92
93 return newOrder;
94}
95
TheCodedProff4facde2023-01-28 13:42:48 -050096const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050097
98 let {name, description} = data;
99 const modal = new ModalBuilder()
100 .setTitle("Edit Name and Description")
101 .setCustomId("editNameDescription")
102 .addComponents(
103 new ActionRowBuilder<TextInputBuilder>()
104 .addComponents(
105 new TextInputBuilder()
TheCodedProff4facde2023-01-28 13:42:48 -0500106 .setLabel("Name")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500107 .setCustomId("name")
TheCodedProff4facde2023-01-28 13:42:48 -0500108 .setPlaceholder("Name here...") // TODO: Make better placeholder
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500109 .setStyle(TextInputStyle.Short)
TheCodedProff4facde2023-01-28 13:42:48 -0500110 .setValue(name ?? "")
111 .setRequired(true)
112 ),
113 new ActionRowBuilder<TextInputBuilder>()
114 .addComponents(
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500115 new TextInputBuilder()
TheCodedProff4facde2023-01-28 13:42:48 -0500116 .setLabel("Description")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500117 .setCustomId("description")
TheCodedProff4facde2023-01-28 13:42:48 -0500118 .setPlaceholder("Description here...") // TODO: Make better placeholder
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500119 .setStyle(TextInputStyle.Short)
TheCodedProff4facde2023-01-28 13:42:48 -0500120 .setValue(description ?? "")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500121 )
122 )
123 const button = new ActionRowBuilder<ButtonBuilder>()
124 .addComponents(
125 new ButtonBuilder()
126 .setCustomId("back")
127 .setLabel("Back")
128 .setStyle(ButtonStyle.Secondary)
129 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
130 )
131
TheCodedProff4facde2023-01-28 13:42:48 -0500132 await i.showModal(modal)
133 await interaction.editReply({
134 embeds: [
135 new EmojiEmbed()
136 .setTitle("Role Menu")
137 .setDescription("Modal opened. If you can't see it, click back and try again.")
138 .setStatus("Success")
139 ],
140 components: [button]
141 });
142
143 let out: Discord.ModalSubmitInteraction | null;
144 try {
145 out = await modalInteractionCollector(
146 m,
147 (m) => m.channel!.id === interaction.channel!.id,
148 (_) => true
149 ) as Discord.ModalSubmitInteraction | null;
150 } catch (e) {
151 console.error(e);
152 out = null;
153 }
154 if(!out) return [name, description];
155 if (out.isButton()) return [name, description];
156 if(!out.fields) return [name, description];
157 name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
158 description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500159 return [name, description]
160
161}
162
163const ellipsis = (str: string, max: number): string => {
164 if (str.length <= max) return str;
165 return str.slice(0, max - 3) + "...";
166}
167
TheCodedProff4facde2023-01-28 13:42:48 -0500168const createRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500169 if (!data) data = {
170 name: "Role Menu Page",
171 description: "A new role menu page",
172 min: 0,
173 max: 0,
174 options: []
175 };
176 const buttons = new ActionRowBuilder<ButtonBuilder>()
177 .addComponents(
178 new ButtonBuilder()
179 .setCustomId("back")
180 .setLabel("Back")
181 .setStyle(ButtonStyle.Secondary)
182 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
183 new ButtonBuilder()
184 .setCustomId("edit")
185 .setLabel("Edit")
186 .setStyle(ButtonStyle.Primary)
187 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
188 new ButtonBuilder()
189 .setCustomId("addRole")
190 .setLabel("Add Role")
191 .setStyle(ButtonStyle.Secondary)
192 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
193 );
194
195 let back = false
TheCodedProff4facde2023-01-28 13:42:48 -0500196 if(data.options.length === 0) {
197 data.options = [
198 {name: "Role 1", description: null, role: "No role set"}
199 ]
200 }
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500201 do {
202 const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options});
203 const embed = new EmojiEmbed()
204 .setTitle(`${data.name}`)
205 .setStatus("Success")
206 .setDescription(
207 `**Description:**\n> ${data.description}\n\n` +
208 `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" +
209 `**Max:** ${data.max}\n`
210 )
211
212 interaction.editReply({embeds: [embed], components: [previewSelect, buttons]});
213 let i: StringSelectMenuInteraction | ButtonInteraction;
214 try {
215 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;
216 } catch (e) {
217 back = true;
218 break;
219 }
220
221 if (i.isStringSelectMenu()) {
222 if(i.customId === "roles") {
223 await i.deferUpdate();
224 await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0]));
225 }
226 } else if (i.isButton()) {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500227 switch (i.customId) {
228 case "back":
TheCodedProff4facde2023-01-28 13:42:48 -0500229 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500230 back = true;
231 break;
232 case "edit":
TheCodedProff4facde2023-01-28 13:42:48 -0500233 let [name, description] = await editNameDescription(i, interaction, m, data);
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500234 data.name = name ? name : data.name;
235 data.description = description ? description : data.description;
236 break;
237 case "addRole":
TheCodedProff4facde2023-01-28 13:42:48 -0500238 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500239 data.options.push(await createRoleMenuOptionPage(interaction, m));
240 break;
241 }
242 }
243
244 } while (!back);
TheCodedProff4facde2023-01-28 13:42:48 -0500245 if(isEqual(data, defaultRolePageConfig)) return null;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500246 return data;
247}
248
249const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => {
250 const { renderRole} = client.logger;
251 if (!data) data = {
252 name: "Role Menu Option",
253 description: null,
254 role: "No role set"
255 };
256 let back = false;
257 const buttons = new ActionRowBuilder<ButtonBuilder>()
258 .addComponents(
259 new ButtonBuilder()
260 .setCustomId("back")
261 .setLabel("Back")
262 .setStyle(ButtonStyle.Secondary)
263 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
264 new ButtonBuilder()
265 .setCustomId("edit")
266 .setLabel("Edit Details")
267 .setStyle(ButtonStyle.Primary)
268 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji)
269 );
270 do {
271 const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role");
272 const embed = new EmojiEmbed()
273 .setTitle(`${data.name ?? "New Role Menu Option"}`)
274 .setStatus("Success")
275 .setDescription(
276 `**Description:**\n> ${data.description ?? "No description set"}\n\n` +
277 `**Role:** ${renderRole((await interaction.guild!.roles.fetch(data.role))!) ?? "No role set"}\n`
278 )
279
280 interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]});
281
282 let i: RoleSelectMenuInteraction | ButtonInteraction;
283 try {
284 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 | RoleSelectMenuInteraction;
285 } catch (e) {
286 back = true;
287 break;
288 }
289
290 if (i.isRoleSelectMenu()) {
291 if(i.customId === "role") {
292 await i.deferUpdate();
293 data.role = (i as RoleSelectMenuInteraction).values[0]!;
294 }
295 } else if (i.isButton()) {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500296 switch (i.customId) {
297 case "back":
TheCodedProff4facde2023-01-28 13:42:48 -0500298 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500299 back = true;
300 break;
301 case "edit":
302 await i.deferUpdate();
TheCodedProff4facde2023-01-28 13:42:48 -0500303 let [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string});
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500304 data.name = name ? name : data.name;
305 data.description = description ? description : data.description;
306 break;
307 }
308 }
309 } while (!back);
310 return data;
311}
pineafanda6e5342022-07-03 10:03:16 +0100312
pineafan63fc5e22022-08-04 22:04:10 +0100313const callback = async (interaction: CommandInteraction): Promise<void> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500314 if (!interaction.guild) return;
315 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
316
317 let page = 0;
318 let closed = false;
319 const config = await client.database.guilds.read(interaction.guild.id);
320 let currentObject: ObjectSchema[] = config.roleMenu.options;
321 let modified = false;
322 do {
323 const embed = new EmojiEmbed()
324 .setTitle("Role Menu Settings")
325 .setEmoji("GUILD.GREEN")
326 .setStatus("Success");
327 const noRoleMenus = currentObject.length === 0;
328 let current: ObjectSchema;
329
330 const pageSelect = new StringSelectMenuBuilder()
331 .setCustomId("page")
332 .setPlaceholder("Select a Role Menu page to manage");
333 const actionSelect = new StringSelectMenuBuilder()
334 .setCustomId("action")
335 .setPlaceholder("Perform an action")
336 .addOptions(
337 new StringSelectMenuOptionBuilder()
338 .setLabel("Edit")
339 .setDescription("Edit this page")
340 .setValue("edit")
341 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
342 new StringSelectMenuOptionBuilder()
343 .setLabel("Delete")
344 .setDescription("Delete this page")
345 .setValue("delete")
346 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
347 );
348 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
349 .addComponents(
350 new ButtonBuilder()
351 .setCustomId("back")
352 .setStyle(ButtonStyle.Primary)
353 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
354 .setDisabled(page === 0),
355 new ButtonBuilder()
356 .setCustomId("next")
357 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
358 .setStyle(ButtonStyle.Primary)
359 .setDisabled(page === Object.keys(currentObject).length - 1),
360 new ButtonBuilder()
361 .setCustomId("add")
362 .setLabel("New Page")
363 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
364 .setStyle(ButtonStyle.Secondary)
365 .setDisabled(Object.keys(currentObject).length >= 24),
366 new ButtonBuilder()
367 .setCustomId("reorder")
368 .setLabel("Reorder Pages")
369 .setEmoji(getEmojiByName("ICONS.SHUFFLE", "id") as APIMessageComponentEmoji)
370 .setStyle(ButtonStyle.Secondary)
371 .setDisabled(Object.keys(currentObject).length <= 1),
372 new ButtonBuilder()
373 .setCustomId("save")
374 .setLabel("Save")
375 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
376 .setStyle(ButtonStyle.Success)
377 .setDisabled(!modified),
378 );
379 if(noRoleMenus) {
380 embed.setDescription("No role menu page have been set up yet. Use the button below to add one.\n\n" +
381 createPageIndicator(1, 1, undefined, true)
382 );
383 pageSelect.setDisabled(true);
384 actionSelect.setDisabled(true);
385 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
386 .setLabel("No role menu pages")
387 .setValue("none")
388 );
389 } else {
390 page = Math.min(page, Object.keys(currentObject).length - 1);
391 current = currentObject[page]!;
392 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
393 `**Description:** \`${current.description}\`\n` +
394 `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}`
395 );
396
397 pageSelect.addOptions(
398 currentObject.map((key: ObjectSchema, index) => {
399 return new StringSelectMenuOptionBuilder()
400 .setLabel(ellipsis(key.name, 50))
401 .setDescription(ellipsis(key.description, 50))
402 .setValue(index.toString());
403 })
404 );
405
406 }
407
408 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
409 let i: StringSelectMenuInteraction | ButtonInteraction;
410 try {
411 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;
412 } catch (e) {
413 closed = true;
414 break;
415 }
416
417 await i.deferUpdate();
418 if (i.isButton()) {
419 switch (i.customId) {
420 case "back":
421 page--;
422 break;
423 case "next":
424 page++;
425 break;
426 case "add":
TheCodedProff4facde2023-01-28 13:42:48 -0500427 let newPage = await createRoleMenuPage(i, m)
428 if(!newPage) break;
429 currentObject.push();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500430 page = currentObject.length - 1;
431 break;
432 case "reorder":
TheCodedProfa112f612023-01-28 18:06:45 -0500433 let reordered = await reorderRoleMenuPages(interaction, m, currentObject);
434 if(!reordered) break;
435 currentObject = reordered;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500436 break;
437 case "save":
438 client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject});
439 modified = false;
440 break;
441 }
442 } else if (i.isStringSelectMenu()) {
443 switch (i.customId) {
444 case "action":
445 switch(i.values[0]) {
446 case "edit":
TheCodedProff4facde2023-01-28 13:42:48 -0500447 let edited = await createRoleMenuPage(i, m, current!);
448 if(!edited) break;
449 currentObject[page] = edited;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500450 modified = true;
451 break;
452 case "delete":
TheCodedProff4facde2023-01-28 13:42:48 -0500453 if(page === 0 && currentObject.keys.length - 1 > 0) page++;
454 else page--;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500455 currentObject.splice(page, 1);
456 break;
457 }
458 break;
459 case "page":
460 page = parseInt(i.values[0]!);
461 break;
462 }
463 }
464
465 } while (!closed)
pineafan63fc5e22022-08-04 22:04:10 +0100466};
pineafanda6e5342022-07-03 10:03:16 +0100467
TheCodedProff86ba092023-01-27 17:10:07 -0500468const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100469 const member = interaction.member as Discord.GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000470 if (!member.permissions.has("ManageRoles"))
471 return "You must have the *Manage Roles* permission to use this command";
pineafanda6e5342022-07-03 10:03:16 +0100472 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100473};
pineafanda6e5342022-07-03 10:03:16 +0100474
475export { command };
476export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100477export { check };