blob: 90b224d79236dda549c9be1bc89356f56ce0b81d [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";
TheCodedProfb5e9d552023-01-29 15:43:26 -050011import ellipsis from "../../utils/ellipsis.js";
TheCodedProff4facde2023-01-28 13:42:48 -050012import lodash from 'lodash';
TheCodedProfa112f612023-01-28 18:06:45 -050013
TheCodedProff4facde2023-01-28 13:42:48 -050014const isEqual = lodash.isEqual;
TheCodedProfa112f612023-01-28 18:06:45 -050015
pineafanda6e5342022-07-03 10:03:16 +010016const command = (builder: SlashCommandSubcommandBuilder) =>
17 builder
pineafan63fc5e22022-08-04 22:04:10 +010018 .setName("rolemenu")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050019 .setDescription("rolemenu")
20
21interface ObjectSchema {
22 name: string;
23 description: string;
24 min: number;
25 max: number;
26 options: {
27 name: string;
28 description: string | null;
29 role: string;
30 }[];
31}
32
TheCodedProff4facde2023-01-28 13:42:48 -050033const defaultRolePageConfig = {
34 name: "Role Menu Page",
35 description: "A new role menu page",
36 min: 0,
37 max: 0,
38 options: [
39 {name: "Role 1", description: null, role: "No role set"}
40 ]
41}
42
TheCodedProfa112f612023-01-28 18:06:45 -050043const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, currentObj: ObjectSchema[]) => {
44 let reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
45 .addComponents(
46 new StringSelectMenuBuilder()
47 .setCustomId("reorder")
TheCodedProfb5e9d552023-01-29 15:43:26 -050048 .setPlaceholder("Select all pages in the order you want them to appear.")
49 .setMinValues(currentObj.length)
50 .setMaxValues(currentObj.length)
TheCodedProfa112f612023-01-28 18:06:45 -050051 .addOptions(
52 currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
53 .setLabel(o.name)
54 .setValue(i.toString())
55 )
56 )
57 );
58 let buttonRow = new ActionRowBuilder<ButtonBuilder>()
59 .addComponents(
60 new ButtonBuilder()
61 .setCustomId("back")
62 .setLabel("Back")
63 .setStyle(ButtonStyle.Secondary)
64 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
65 )
66 await interaction.editReply({
67 embeds: [
68 new EmojiEmbed()
69 .setTitle("Role Menu")
70 .setDescription("Select pages in the order you want them to appear.")
71 .setStatus("Success")
72 ],
73 components: [reorderRow, buttonRow]
74 });
75 let out: StringSelectMenuInteraction | ButtonInteraction | null;
76 try {
77 out = await m.awaitMessageComponent({
78 filter: (i) => i.channel!.id === interaction.channel!.id,
79 time: 300000
80 }) as StringSelectMenuInteraction | ButtonInteraction | null;
81 } catch (e) {
82 console.error(e);
83 out = null;
84 }
85 if(!out) return;
TheCodedProfb5e9d552023-01-29 15:43:26 -050086 out.deferUpdate();
TheCodedProfa112f612023-01-28 18:06:45 -050087 if (out.isButton()) return;
88 if(!out.values) return;
89 const values = out.values;
90
91 const newOrder: ObjectSchema[] = currentObj.map((_, i) => {
92 const index = values.findIndex(v => v === i.toString());
93 return currentObj[index];
94 }) as ObjectSchema[];
95
96 return newOrder;
97}
98
TheCodedProff4facde2023-01-28 13:42:48 -050099const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500100
101 let {name, description} = data;
102 const modal = new ModalBuilder()
103 .setTitle("Edit Name and Description")
104 .setCustomId("editNameDescription")
105 .addComponents(
106 new ActionRowBuilder<TextInputBuilder>()
107 .addComponents(
108 new TextInputBuilder()
TheCodedProff4facde2023-01-28 13:42:48 -0500109 .setLabel("Name")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500110 .setCustomId("name")
TheCodedProff4facde2023-01-28 13:42:48 -0500111 .setPlaceholder("Name here...") // TODO: Make better placeholder
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500112 .setStyle(TextInputStyle.Short)
TheCodedProff4facde2023-01-28 13:42:48 -0500113 .setValue(name ?? "")
114 .setRequired(true)
115 ),
116 new ActionRowBuilder<TextInputBuilder>()
117 .addComponents(
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500118 new TextInputBuilder()
TheCodedProff4facde2023-01-28 13:42:48 -0500119 .setLabel("Description")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500120 .setCustomId("description")
TheCodedProff4facde2023-01-28 13:42:48 -0500121 .setPlaceholder("Description here...") // TODO: Make better placeholder
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500122 .setStyle(TextInputStyle.Short)
TheCodedProff4facde2023-01-28 13:42:48 -0500123 .setValue(description ?? "")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500124 )
125 )
126 const button = new ActionRowBuilder<ButtonBuilder>()
127 .addComponents(
128 new ButtonBuilder()
129 .setCustomId("back")
130 .setLabel("Back")
131 .setStyle(ButtonStyle.Secondary)
132 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
133 )
134
TheCodedProff4facde2023-01-28 13:42:48 -0500135 await i.showModal(modal)
136 await interaction.editReply({
137 embeds: [
138 new EmojiEmbed()
139 .setTitle("Role Menu")
140 .setDescription("Modal opened. If you can't see it, click back and try again.")
141 .setStatus("Success")
142 ],
143 components: [button]
144 });
145
146 let out: Discord.ModalSubmitInteraction | null;
147 try {
148 out = await modalInteractionCollector(
149 m,
150 (m) => m.channel!.id === interaction.channel!.id,
151 (_) => true
152 ) as Discord.ModalSubmitInteraction | null;
153 } catch (e) {
154 console.error(e);
155 out = null;
156 }
157 if(!out) return [name, description];
158 if (out.isButton()) return [name, description];
159 if(!out.fields) return [name, description];
160 name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
161 description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500162 return [name, description]
163
164}
165
TheCodedProfb5e9d552023-01-29 15:43:26 -0500166const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500167 if (!data) data = {
168 name: "Role Menu Page",
169 description: "A new role menu page",
170 min: 0,
171 max: 0,
172 options: []
173 };
174 const buttons = new ActionRowBuilder<ButtonBuilder>()
175 .addComponents(
176 new ButtonBuilder()
177 .setCustomId("back")
178 .setLabel("Back")
179 .setStyle(ButtonStyle.Secondary)
180 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
181 new ButtonBuilder()
182 .setCustomId("edit")
183 .setLabel("Edit")
184 .setStyle(ButtonStyle.Primary)
185 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
186 new ButtonBuilder()
187 .setCustomId("addRole")
188 .setLabel("Add Role")
189 .setStyle(ButtonStyle.Secondary)
190 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
191 );
192
193 let back = false
TheCodedProff4facde2023-01-28 13:42:48 -0500194 if(data.options.length === 0) {
195 data.options = [
196 {name: "Role 1", description: null, role: "No role set"}
197 ]
198 }
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500199 do {
200 const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options});
201 const embed = new EmojiEmbed()
202 .setTitle(`${data.name}`)
203 .setStatus("Success")
204 .setDescription(
205 `**Description:**\n> ${data.description}\n\n` +
206 `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" +
207 `**Max:** ${data.max}\n`
208 )
209
210 interaction.editReply({embeds: [embed], components: [previewSelect, buttons]});
211 let i: StringSelectMenuInteraction | ButtonInteraction;
212 try {
213 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;
214 } catch (e) {
215 back = true;
216 break;
217 }
218
219 if (i.isStringSelectMenu()) {
220 if(i.customId === "roles") {
221 await i.deferUpdate();
222 await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0]));
223 }
224 } else if (i.isButton()) {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500225 switch (i.customId) {
226 case "back":
TheCodedProff4facde2023-01-28 13:42:48 -0500227 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500228 back = true;
229 break;
230 case "edit":
TheCodedProff4facde2023-01-28 13:42:48 -0500231 let [name, description] = await editNameDescription(i, interaction, m, data);
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500232 data.name = name ? name : data.name;
233 data.description = description ? description : data.description;
234 break;
235 case "addRole":
TheCodedProff4facde2023-01-28 13:42:48 -0500236 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500237 data.options.push(await createRoleMenuOptionPage(interaction, m));
238 break;
239 }
240 }
241
242 } while (!back);
TheCodedProff4facde2023-01-28 13:42:48 -0500243 if(isEqual(data, defaultRolePageConfig)) return null;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500244 return data;
245}
246
247const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => {
248 const { renderRole} = client.logger;
249 if (!data) data = {
250 name: "Role Menu Option",
251 description: null,
252 role: "No role set"
253 };
254 let back = false;
255 const buttons = new ActionRowBuilder<ButtonBuilder>()
256 .addComponents(
257 new ButtonBuilder()
258 .setCustomId("back")
259 .setLabel("Back")
260 .setStyle(ButtonStyle.Secondary)
261 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
262 new ButtonBuilder()
263 .setCustomId("edit")
264 .setLabel("Edit Details")
265 .setStyle(ButtonStyle.Primary)
266 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji)
267 );
268 do {
269 const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role");
270 const embed = new EmojiEmbed()
271 .setTitle(`${data.name ?? "New Role Menu Option"}`)
272 .setStatus("Success")
273 .setDescription(
274 `**Description:**\n> ${data.description ?? "No description set"}\n\n` +
275 `**Role:** ${renderRole((await interaction.guild!.roles.fetch(data.role))!) ?? "No role set"}\n`
276 )
277
278 interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]});
279
280 let i: RoleSelectMenuInteraction | ButtonInteraction;
281 try {
282 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;
283 } catch (e) {
284 back = true;
285 break;
286 }
287
288 if (i.isRoleSelectMenu()) {
289 if(i.customId === "role") {
290 await i.deferUpdate();
291 data.role = (i as RoleSelectMenuInteraction).values[0]!;
292 }
293 } else if (i.isButton()) {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500294 switch (i.customId) {
295 case "back":
TheCodedProff4facde2023-01-28 13:42:48 -0500296 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500297 back = true;
298 break;
299 case "edit":
300 await i.deferUpdate();
TheCodedProff4facde2023-01-28 13:42:48 -0500301 let [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string});
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500302 data.name = name ? name : data.name;
303 data.description = description ? description : data.description;
304 break;
305 }
306 }
307 } while (!back);
308 return data;
309}
pineafanda6e5342022-07-03 10:03:16 +0100310
pineafan63fc5e22022-08-04 22:04:10 +0100311const callback = async (interaction: CommandInteraction): Promise<void> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500312 if (!interaction.guild) return;
313 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
314
315 let page = 0;
316 let closed = false;
317 const config = await client.database.guilds.read(interaction.guild.id);
318 let currentObject: ObjectSchema[] = config.roleMenu.options;
319 let modified = false;
320 do {
321 const embed = new EmojiEmbed()
TheCodedProfb5e9d552023-01-29 15:43:26 -0500322 .setTitle("Role Menu")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500323 .setEmoji("GUILD.GREEN")
324 .setStatus("Success");
325 const noRoleMenus = currentObject.length === 0;
326 let current: ObjectSchema;
327
328 const pageSelect = new StringSelectMenuBuilder()
329 .setCustomId("page")
330 .setPlaceholder("Select a Role Menu page to manage");
331 const actionSelect = new StringSelectMenuBuilder()
332 .setCustomId("action")
333 .setPlaceholder("Perform an action")
334 .addOptions(
335 new StringSelectMenuOptionBuilder()
336 .setLabel("Edit")
337 .setDescription("Edit this page")
338 .setValue("edit")
339 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
340 new StringSelectMenuOptionBuilder()
341 .setLabel("Delete")
342 .setDescription("Delete this page")
343 .setValue("delete")
344 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
345 );
346 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
347 .addComponents(
348 new ButtonBuilder()
349 .setCustomId("back")
350 .setStyle(ButtonStyle.Primary)
351 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
352 .setDisabled(page === 0),
353 new ButtonBuilder()
354 .setCustomId("next")
355 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
356 .setStyle(ButtonStyle.Primary)
357 .setDisabled(page === Object.keys(currentObject).length - 1),
358 new ButtonBuilder()
359 .setCustomId("add")
360 .setLabel("New Page")
361 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
362 .setStyle(ButtonStyle.Secondary)
363 .setDisabled(Object.keys(currentObject).length >= 24),
364 new ButtonBuilder()
365 .setCustomId("reorder")
366 .setLabel("Reorder Pages")
TheCodedProf4f79da12023-01-31 16:50:37 -0500367 .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji)
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500368 .setStyle(ButtonStyle.Secondary)
369 .setDisabled(Object.keys(currentObject).length <= 1),
370 new ButtonBuilder()
371 .setCustomId("save")
372 .setLabel("Save")
373 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
374 .setStyle(ButtonStyle.Success)
375 .setDisabled(!modified),
376 );
377 if(noRoleMenus) {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500378 embed.setDescription("No role menu pages have been set up yet. Use the button below to add one.\n\n" +
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500379 createPageIndicator(1, 1, undefined, true)
380 );
381 pageSelect.setDisabled(true);
382 actionSelect.setDisabled(true);
383 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
384 .setLabel("No role menu pages")
385 .setValue("none")
386 );
387 } else {
388 page = Math.min(page, Object.keys(currentObject).length - 1);
389 current = currentObject[page]!;
390 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
TheCodedProfb5e9d552023-01-29 15:43:26 -0500391 `**Description:**\n> ${current.description}\n` +
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500392 `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}`
393 );
394
395 pageSelect.addOptions(
396 currentObject.map((key: ObjectSchema, index) => {
397 return new StringSelectMenuOptionBuilder()
398 .setLabel(ellipsis(key.name, 50))
399 .setDescription(ellipsis(key.description, 50))
400 .setValue(index.toString());
401 })
402 );
403
404 }
405
406 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
407 let i: StringSelectMenuInteraction | ButtonInteraction;
408 try {
409 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;
410 } catch (e) {
411 closed = true;
412 break;
413 }
414
415 await i.deferUpdate();
416 if (i.isButton()) {
417 switch (i.customId) {
418 case "back":
419 page--;
420 break;
421 case "next":
422 page++;
423 break;
424 case "add":
TheCodedProfb5e9d552023-01-29 15:43:26 -0500425 let newPage = await editRoleMenuPage(i, m)
TheCodedProff4facde2023-01-28 13:42:48 -0500426 if(!newPage) break;
427 currentObject.push();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500428 page = currentObject.length - 1;
429 break;
430 case "reorder":
TheCodedProfa112f612023-01-28 18:06:45 -0500431 let reordered = await reorderRoleMenuPages(interaction, m, currentObject);
432 if(!reordered) break;
433 currentObject = reordered;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500434 break;
435 case "save":
436 client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject});
437 modified = false;
438 break;
439 }
440 } else if (i.isStringSelectMenu()) {
441 switch (i.customId) {
442 case "action":
443 switch(i.values[0]) {
444 case "edit":
TheCodedProfb5e9d552023-01-29 15:43:26 -0500445 let edited = await editRoleMenuPage(i, m, current!);
TheCodedProff4facde2023-01-28 13:42:48 -0500446 if(!edited) break;
447 currentObject[page] = edited;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500448 modified = true;
449 break;
450 case "delete":
TheCodedProff4facde2023-01-28 13:42:48 -0500451 if(page === 0 && currentObject.keys.length - 1 > 0) page++;
452 else page--;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500453 currentObject.splice(page, 1);
454 break;
455 }
456 break;
457 case "page":
458 page = parseInt(i.values[0]!);
459 break;
460 }
461 }
462
463 } while (!closed)
pineafan63fc5e22022-08-04 22:04:10 +0100464};
pineafanda6e5342022-07-03 10:03:16 +0100465
TheCodedProff86ba092023-01-27 17:10:07 -0500466const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100467 const member = interaction.member as Discord.GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000468 if (!member.permissions.has("ManageRoles"))
469 return "You must have the *Manage Roles* permission to use this command";
pineafanda6e5342022-07-03 10:03:16 +0100470 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100471};
pineafanda6e5342022-07-03 10:03:16 +0100472
473export { command };
474export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100475export { check };