blob: cccb6f6f74b84ce090463b2c77d8c9f7353a4f16 [file] [log] [blame]
TheCodedProfafca98b2023-01-17 22:25:43 -05001import type Discord from "discord.js";
Samuel Shuert27bf3cd2023-03-03 15:51:25 -05002import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, CommandInteraction, Message, ModalBuilder, RoleSelectMenuBuilder, RoleSelectMenuInteraction, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
3import type { SlashCommandSubcommandBuilder } from "discord.js";
4import 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";
10import { modalInteractionCollector } from "../../utils/dualCollector.js";
11import ellipsis from "../../utils/ellipsis.js";
12import lodash from 'lodash';
13
14const isEqual = lodash.isEqual;
pineafanda6e5342022-07-03 10:03:16 +010015
16const command = (builder: SlashCommandSubcommandBuilder) =>
17 builder
pineafan63fc5e22022-08-04 22:04:10 +010018 .setName("rolemenu")
Samuel Shuert27bf3cd2023-03-03 15:51:25 -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
33const 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
43const reorderRoleMenuPages = async (interaction: CommandInteraction, m: Message, currentObj: ObjectSchema[]) => {
44 const reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
45 .addComponents(
46 new StringSelectMenuBuilder()
47 .setCustomId("reorder")
48 .setPlaceholder("Select all pages in the order you want them to appear.")
49 .setMinValues(currentObj.length)
50 .setMaxValues(currentObj.length)
51 .addOptions(
52 currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
53 .setLabel(o.name)
54 .setValue(i.toString())
55 )
56 )
57 );
58 const 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;
86 out.deferUpdate();
87 if (out.isButton()) return;
88 const values = out.values;
89
90 const newOrder: ObjectSchema[] = currentObj.map((_, i) => {
91 const index = values.findIndex(v => v === i.toString());
92 return currentObj[index];
93 }) as ObjectSchema[];
94
95 return newOrder;
96}
97
98const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => {
99
100 let {name, description} = data;
101 const modal = new ModalBuilder()
102 .setTitle("Edit Name and Description")
103 .setCustomId("editNameDescription")
104 .addComponents(
105 new ActionRowBuilder<TextInputBuilder>()
106 .addComponents(
107 new TextInputBuilder()
108 .setLabel("Name")
109 .setCustomId("name")
110 .setPlaceholder("The name of the role (e.g. Programmer)")
111 .setStyle(TextInputStyle.Short)
112 .setValue(name ?? "")
113 .setRequired(true)
114 ),
115 new ActionRowBuilder<TextInputBuilder>()
116 .addComponents(
117 new TextInputBuilder()
118 .setLabel("Description")
119 .setCustomId("description")
120 .setPlaceholder("A short description of the role (e.g. A role for people who code)")
121 .setStyle(TextInputStyle.Short)
122 .setValue(description ?? "")
123 )
124 )
125 const button = new ActionRowBuilder<ButtonBuilder>()
126 .addComponents(
127 new ButtonBuilder()
128 .setCustomId("back")
129 .setLabel("Back")
130 .setStyle(ButtonStyle.Secondary)
131 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
132 )
133
134 await i.showModal(modal)
135 await interaction.editReply({
136 embeds: [
137 new EmojiEmbed()
138 .setTitle("Role Menu")
139 .setDescription("Modal opened. If you can't see it, click back and try again.")
140 .setStatus("Success")
141 ],
142 components: [button]
143 });
144
145 let out: Discord.ModalSubmitInteraction | null;
146 try {
147 out = await modalInteractionCollector(m, interaction.user) as Discord.ModalSubmitInteraction | null;
148 } catch (e) {
149 console.error(e);
150 out = null;
151 }
152 if(!out) return [name, description];
153 if (out.isButton()) return [name, description];
154 name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
155 description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description;
156 return [name, description]
157
158}
159
160const editRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => {
161 if (!data) data = {
162 name: "Role Menu Page",
163 description: "A new role menu page",
164 min: 0,
165 max: 0,
166 options: []
167 };
168 const buttons = new ActionRowBuilder<ButtonBuilder>()
169 .addComponents(
170 new ButtonBuilder()
171 .setCustomId("back")
172 .setLabel("Back")
173 .setStyle(ButtonStyle.Secondary)
174 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
175 new ButtonBuilder()
176 .setCustomId("edit")
177 .setLabel("Edit")
178 .setStyle(ButtonStyle.Primary)
179 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
180 new ButtonBuilder()
181 .setCustomId("addRole")
182 .setLabel("Add Role")
183 .setStyle(ButtonStyle.Secondary)
184 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
185 );
186
187 let back = false
188 if(data.options.length === 0) {
189 data.options = [
190 {name: "Role 1", description: null, role: "No role set"}
191 ]
192 }
193 do {
194 const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options});
195 const embed = new EmojiEmbed()
196 .setTitle(`${data.name}`)
197 .setStatus("Success")
198 .setDescription(
199 `**Description:**\n> ${data.description}\n\n` +
200 `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" +
201 `**Max:** ${data.max}\n`
202 )
203
204 interaction.editReply({embeds: [embed], components: [previewSelect, buttons]});
205 let i: StringSelectMenuInteraction | ButtonInteraction;
206 try {
207 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;
208 } catch (e) {
209 back = true;
210 break;
211 }
212
213 if (i.isStringSelectMenu()) {
214 if(i.customId === "roles") {
215 await i.deferUpdate();
216 await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0]));
217 }
218 } else if (i.isButton()) {
219 switch (i.customId) {
220 case "back": {
221 await i.deferUpdate();
222 back = true;
223 break;
224 }
225 case "edit": {
226 const [name, description] = await editNameDescription(i, interaction, m, data);
227 data.name = name ? name : data.name;
228 data.description = description ? description : data.description;
229 break;
230 }
231 case "addRole": {
232 await i.deferUpdate();
233 data.options.push(await createRoleMenuOptionPage(interaction, m));
234 break;
235 }
236 }
237 }
238
239 } while (!back);
240 if(isEqual(data, defaultRolePageConfig)) return null;
241 return data;
242}
243
244const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => {
245 const { renderRole} = client.logger;
246 if (!data) data = {
247 name: "New role Menu Option",
248 description: null,
249 role: ""
250 };
251 let back = false;
252 const buttons = new ActionRowBuilder<ButtonBuilder>()
253 .addComponents(
254 new ButtonBuilder()
255 .setCustomId("back")
256 .setLabel("Back")
257 .setStyle(ButtonStyle.Secondary)
258 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
259 new ButtonBuilder()
260 .setCustomId("edit")
261 .setLabel("Edit Details")
262 .setStyle(ButtonStyle.Primary)
263 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji)
264 );
265 do {
266 const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role");
267 const embed = new EmojiEmbed()
268 .setTitle(`${data.name}`)
269 .setStatus("Success")
270 .setDescription(
271 `**Description:**\n> ${data.description ?? "No description set"}\n\n` +
272 `**Role:** ${data.role ? renderRole((await interaction.guild!.roles.fetch(data.role))!) : "No role set"}\n`
273 )
274
275 interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]});
276
277 let i: RoleSelectMenuInteraction | ButtonInteraction;
278 try {
279 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;
280 } catch (e) {
281 back = true;
282 break;
283 }
284
285 if (i.isRoleSelectMenu()) {
286 if(i.customId === "role") {
287 await i.deferUpdate();
288 data.role = (i as RoleSelectMenuInteraction).values[0]!;
289 }
290 } else if (i.isButton()) {
291 switch (i.customId) {
292 case "back": {
293 await i.deferUpdate();
294 back = true;
295 break;
296 }
297 case "edit": {
298 await i.deferUpdate();
299 const [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string});
300 data.name = name ? name : data.name;
301 data.description = description ? description : data.description;
302 break;
303 }
304 }
305 }
306 } while (!back);
307 return data;
308}
pineafanda6e5342022-07-03 10:03:16 +0100309
pineafan63fc5e22022-08-04 22:04:10 +0100310const callback = async (interaction: CommandInteraction): Promise<void> => {
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500311 if (!interaction.guild) return;
312 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
313
314 let page = 0;
315 let closed = false;
316 const config = await client.database.guilds.read(interaction.guild.id);
317 let currentObject: ObjectSchema[] = config.roleMenu.options;
318 let modified = false;
319 do {
320 const embed = new EmojiEmbed()
321 .setTitle("Role Menu")
322 .setEmoji("GUILD.GREEN")
323 .setStatus("Success");
324 const noRoleMenus = currentObject.length === 0;
325 let current: ObjectSchema;
326
327 const pageSelect = new StringSelectMenuBuilder()
328 .setCustomId("page")
329 .setPlaceholder("Select a Role Menu page to manage");
330 const actionSelect = new StringSelectMenuBuilder()
331 .setCustomId("action")
332 .setPlaceholder("Perform an action")
333 .addOptions(
334 new StringSelectMenuOptionBuilder()
335 .setLabel("Edit")
336 .setDescription("Edit this page")
337 .setValue("edit")
338 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
339 new StringSelectMenuOptionBuilder()
340 .setLabel("Delete")
341 .setDescription("Delete this page")
342 .setValue("delete")
343 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
344 );
345 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
346 .addComponents(
347 new ButtonBuilder()
348 .setCustomId("back")
349 .setStyle(ButtonStyle.Primary)
350 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
351 .setDisabled(page === 0),
352 new ButtonBuilder()
353 .setCustomId("next")
354 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
355 .setStyle(ButtonStyle.Primary)
356 .setDisabled(page === Object.keys(currentObject).length - 1),
357 new ButtonBuilder()
358 .setCustomId("add")
359 .setLabel("New Page")
360 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
361 .setStyle(ButtonStyle.Secondary)
362 .setDisabled(Object.keys(currentObject).length >= 24),
363 new ButtonBuilder()
364 .setCustomId("reorder")
365 .setLabel("Reorder Pages")
366 .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji)
367 .setStyle(ButtonStyle.Secondary)
368 .setDisabled(Object.keys(currentObject).length <= 1),
369 new ButtonBuilder()
370 .setCustomId("save")
371 .setLabel("Save")
372 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
373 .setStyle(ButtonStyle.Success)
374 .setDisabled(!modified),
375 );
376 if(noRoleMenus) {
377 embed.setDescription("No role menu pages have been set up yet. Use the button below to add one.\n\n" +
378 createPageIndicator(1, 1, undefined, true)
379 );
380 pageSelect.setDisabled(true);
381 actionSelect.setDisabled(true);
382 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
383 .setLabel("No role menu pages")
384 .setValue("none")
385 );
386 } else {
387 page = Math.min(page, Object.keys(currentObject).length - 1);
388 current = currentObject[page]!;
389 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
390 `**Description:**\n> ${current.description}\n` +
391 `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}`
392 );
393
394 pageSelect.addOptions(
395 currentObject.map((key: ObjectSchema, index) => {
396 return new StringSelectMenuOptionBuilder()
397 .setLabel(ellipsis(key.name, 50))
398 .setDescription(ellipsis(key.description, 50))
399 .setValue(index.toString());
400 })
401 );
402
403 }
404
405 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
406 let i: StringSelectMenuInteraction | ButtonInteraction;
407 try {
408 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;
409 } catch (e) {
410 closed = true;
411 continue;
412 }
413
414 await i.deferUpdate();
415 if (i.isButton()) {
416 switch (i.customId) {
417 case "back": {
418 page--;
419 break;
420 }
421 case "next": {
422 page++;
423 break;
424 }
425 case "add": {
426 const newPage = await editRoleMenuPage(i, m)
427 if(!newPage) break;
428 currentObject.push();
429 page = currentObject.length - 1;
430 break;
431 }
432 case "reorder": {
433 const reordered = await reorderRoleMenuPages(interaction, m, currentObject);
434 if(!reordered) break;
435 currentObject = reordered;
436 break;
437 }
438 case "save": {
439 client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject});
440 modified = false;
441 break;
442 }
443 }
444 } else if (i.isStringSelectMenu()) {
445 switch (i.customId) {
446 case "action": {
447 switch(i.values[0]) {
448 case "edit": {
449 const edited = await editRoleMenuPage(i, m, current!);
450 if(!edited) break;
451 currentObject[page] = edited;
452 modified = true;
453 break;
454 }
455 case "delete": {
456 if(page === 0 && currentObject.keys.length - 1 > 0) page++;
457 else page--;
458 currentObject.splice(page, 1);
459 break;
460 }
461 }
462 break;
463 }
464 case "page": {
465 page = parseInt(i.values[0]!);
466 break;
467 }
468 }
469 }
470
471 } while (!closed);
472 await interaction.deleteReply()
pineafan63fc5e22022-08-04 22:04:10 +0100473};
pineafanda6e5342022-07-03 10:03:16 +0100474
Samuel Shuert27bf3cd2023-03-03 15:51:25 -0500475const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100476 const member = interaction.member as Discord.GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000477 if (!member.permissions.has("ManageRoles"))
478 return "You must have the *Manage Roles* permission to use this command";
pineafanda6e5342022-07-03 10:03:16 +0100479 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100480};
pineafanda6e5342022-07-03 10:03:16 +0100481
482export { command };
483export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100484export { check };