blob: 1591273facf30b696b4d0cdbd14fc256b4665a6f [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';
12const isEqual = lodash.isEqual;
pineafanda6e5342022-07-03 10:03:16 +010013const command = (builder: SlashCommandSubcommandBuilder) =>
14 builder
pineafan63fc5e22022-08-04 22:04:10 +010015 .setName("rolemenu")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050016 .setDescription("rolemenu")
17
18interface ObjectSchema {
19 name: string;
20 description: string;
21 min: number;
22 max: number;
23 options: {
24 name: string;
25 description: string | null;
26 role: string;
27 }[];
28}
29
TheCodedProff4facde2023-01-28 13:42:48 -050030const defaultRolePageConfig = {
31 name: "Role Menu Page",
32 description: "A new role menu page",
33 min: 0,
34 max: 0,
35 options: [
36 {name: "Role 1", description: null, role: "No role set"}
37 ]
38}
39
40const editNameDescription = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050041
42 let {name, description} = data;
43 const modal = new ModalBuilder()
44 .setTitle("Edit Name and Description")
45 .setCustomId("editNameDescription")
46 .addComponents(
47 new ActionRowBuilder<TextInputBuilder>()
48 .addComponents(
49 new TextInputBuilder()
TheCodedProff4facde2023-01-28 13:42:48 -050050 .setLabel("Name")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050051 .setCustomId("name")
TheCodedProff4facde2023-01-28 13:42:48 -050052 .setPlaceholder("Name here...") // TODO: Make better placeholder
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050053 .setStyle(TextInputStyle.Short)
TheCodedProff4facde2023-01-28 13:42:48 -050054 .setValue(name ?? "")
55 .setRequired(true)
56 ),
57 new ActionRowBuilder<TextInputBuilder>()
58 .addComponents(
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050059 new TextInputBuilder()
TheCodedProff4facde2023-01-28 13:42:48 -050060 .setLabel("Description")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050061 .setCustomId("description")
TheCodedProff4facde2023-01-28 13:42:48 -050062 .setPlaceholder("Description here...") // TODO: Make better placeholder
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050063 .setStyle(TextInputStyle.Short)
TheCodedProff4facde2023-01-28 13:42:48 -050064 .setValue(description ?? "")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050065 )
66 )
67 const button = new ActionRowBuilder<ButtonBuilder>()
68 .addComponents(
69 new ButtonBuilder()
70 .setCustomId("back")
71 .setLabel("Back")
72 .setStyle(ButtonStyle.Secondary)
73 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
74 )
75
TheCodedProff4facde2023-01-28 13:42:48 -050076 await i.showModal(modal)
77 await interaction.editReply({
78 embeds: [
79 new EmojiEmbed()
80 .setTitle("Role Menu")
81 .setDescription("Modal opened. If you can't see it, click back and try again.")
82 .setStatus("Success")
83 ],
84 components: [button]
85 });
86
87 let out: Discord.ModalSubmitInteraction | null;
88 try {
89 out = await modalInteractionCollector(
90 m,
91 (m) => m.channel!.id === interaction.channel!.id,
92 (_) => true
93 ) as Discord.ModalSubmitInteraction | null;
94 } catch (e) {
95 console.error(e);
96 out = null;
97 }
98 if(!out) return [name, description];
99 if (out.isButton()) return [name, description];
100 if(!out.fields) return [name, description];
101 name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
102 description = out.fields.fields.find((f) => f.customId === "description")?.value ?? description;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500103 return [name, description]
104
105}
106
107const ellipsis = (str: string, max: number): string => {
108 if (str.length <= max) return str;
109 return str.slice(0, max - 3) + "...";
110}
111
TheCodedProff4facde2023-01-28 13:42:48 -0500112const createRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema): Promise<ObjectSchema | null> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500113 if (!data) data = {
114 name: "Role Menu Page",
115 description: "A new role menu page",
116 min: 0,
117 max: 0,
118 options: []
119 };
120 const buttons = new ActionRowBuilder<ButtonBuilder>()
121 .addComponents(
122 new ButtonBuilder()
123 .setCustomId("back")
124 .setLabel("Back")
125 .setStyle(ButtonStyle.Secondary)
126 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
127 new ButtonBuilder()
128 .setCustomId("edit")
129 .setLabel("Edit")
130 .setStyle(ButtonStyle.Primary)
131 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
132 new ButtonBuilder()
133 .setCustomId("addRole")
134 .setLabel("Add Role")
135 .setStyle(ButtonStyle.Secondary)
136 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
137 );
138
139 let back = false
TheCodedProff4facde2023-01-28 13:42:48 -0500140 if(data.options.length === 0) {
141 data.options = [
142 {name: "Role 1", description: null, role: "No role set"}
143 ]
144 }
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500145 do {
146 const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options});
147 const embed = new EmojiEmbed()
148 .setTitle(`${data.name}`)
149 .setStatus("Success")
150 .setDescription(
151 `**Description:**\n> ${data.description}\n\n` +
152 `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" +
153 `**Max:** ${data.max}\n`
154 )
155
156 interaction.editReply({embeds: [embed], components: [previewSelect, buttons]});
157 let i: StringSelectMenuInteraction | ButtonInteraction;
158 try {
159 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;
160 } catch (e) {
161 back = true;
162 break;
163 }
164
165 if (i.isStringSelectMenu()) {
166 if(i.customId === "roles") {
167 await i.deferUpdate();
168 await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0]));
169 }
170 } else if (i.isButton()) {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500171 switch (i.customId) {
172 case "back":
TheCodedProff4facde2023-01-28 13:42:48 -0500173 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500174 back = true;
175 break;
176 case "edit":
TheCodedProff4facde2023-01-28 13:42:48 -0500177 let [name, description] = await editNameDescription(i, interaction, m, data);
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500178 data.name = name ? name : data.name;
179 data.description = description ? description : data.description;
180 break;
181 case "addRole":
TheCodedProff4facde2023-01-28 13:42:48 -0500182 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500183 data.options.push(await createRoleMenuOptionPage(interaction, m));
184 break;
185 }
186 }
187
188 } while (!back);
TheCodedProff4facde2023-01-28 13:42:48 -0500189 if(isEqual(data, defaultRolePageConfig)) return null;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500190 return data;
191}
192
193const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => {
194 const { renderRole} = client.logger;
195 if (!data) data = {
196 name: "Role Menu Option",
197 description: null,
198 role: "No role set"
199 };
200 let back = false;
201 const buttons = new ActionRowBuilder<ButtonBuilder>()
202 .addComponents(
203 new ButtonBuilder()
204 .setCustomId("back")
205 .setLabel("Back")
206 .setStyle(ButtonStyle.Secondary)
207 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
208 new ButtonBuilder()
209 .setCustomId("edit")
210 .setLabel("Edit Details")
211 .setStyle(ButtonStyle.Primary)
212 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji)
213 );
214 do {
215 const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role");
216 const embed = new EmojiEmbed()
217 .setTitle(`${data.name ?? "New Role Menu Option"}`)
218 .setStatus("Success")
219 .setDescription(
220 `**Description:**\n> ${data.description ?? "No description set"}\n\n` +
221 `**Role:** ${renderRole((await interaction.guild!.roles.fetch(data.role))!) ?? "No role set"}\n`
222 )
223
224 interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]});
225
226 let i: RoleSelectMenuInteraction | ButtonInteraction;
227 try {
228 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;
229 } catch (e) {
230 back = true;
231 break;
232 }
233
234 if (i.isRoleSelectMenu()) {
235 if(i.customId === "role") {
236 await i.deferUpdate();
237 data.role = (i as RoleSelectMenuInteraction).values[0]!;
238 }
239 } else if (i.isButton()) {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500240 switch (i.customId) {
241 case "back":
TheCodedProff4facde2023-01-28 13:42:48 -0500242 await i.deferUpdate();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500243 back = true;
244 break;
245 case "edit":
246 await i.deferUpdate();
TheCodedProff4facde2023-01-28 13:42:48 -0500247 let [name, description] = await editNameDescription(i, interaction, m, data as {name: string; description: string});
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500248 data.name = name ? name : data.name;
249 data.description = description ? description : data.description;
250 break;
251 }
252 }
253 } while (!back);
254 return data;
255}
pineafanda6e5342022-07-03 10:03:16 +0100256
pineafan63fc5e22022-08-04 22:04:10 +0100257const callback = async (interaction: CommandInteraction): Promise<void> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500258 if (!interaction.guild) return;
259 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
260
261 let page = 0;
262 let closed = false;
263 const config = await client.database.guilds.read(interaction.guild.id);
264 let currentObject: ObjectSchema[] = config.roleMenu.options;
265 let modified = false;
266 do {
267 const embed = new EmojiEmbed()
268 .setTitle("Role Menu Settings")
269 .setEmoji("GUILD.GREEN")
270 .setStatus("Success");
271 const noRoleMenus = currentObject.length === 0;
272 let current: ObjectSchema;
273
274 const pageSelect = new StringSelectMenuBuilder()
275 .setCustomId("page")
276 .setPlaceholder("Select a Role Menu page to manage");
277 const actionSelect = new StringSelectMenuBuilder()
278 .setCustomId("action")
279 .setPlaceholder("Perform an action")
280 .addOptions(
281 new StringSelectMenuOptionBuilder()
282 .setLabel("Edit")
283 .setDescription("Edit this page")
284 .setValue("edit")
285 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
286 new StringSelectMenuOptionBuilder()
287 .setLabel("Delete")
288 .setDescription("Delete this page")
289 .setValue("delete")
290 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
291 );
292 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
293 .addComponents(
294 new ButtonBuilder()
295 .setCustomId("back")
296 .setStyle(ButtonStyle.Primary)
297 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
298 .setDisabled(page === 0),
299 new ButtonBuilder()
300 .setCustomId("next")
301 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
302 .setStyle(ButtonStyle.Primary)
303 .setDisabled(page === Object.keys(currentObject).length - 1),
304 new ButtonBuilder()
305 .setCustomId("add")
306 .setLabel("New Page")
307 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
308 .setStyle(ButtonStyle.Secondary)
309 .setDisabled(Object.keys(currentObject).length >= 24),
310 new ButtonBuilder()
311 .setCustomId("reorder")
312 .setLabel("Reorder Pages")
313 .setEmoji(getEmojiByName("ICONS.SHUFFLE", "id") as APIMessageComponentEmoji)
314 .setStyle(ButtonStyle.Secondary)
315 .setDisabled(Object.keys(currentObject).length <= 1),
316 new ButtonBuilder()
317 .setCustomId("save")
318 .setLabel("Save")
319 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
320 .setStyle(ButtonStyle.Success)
321 .setDisabled(!modified),
322 );
323 if(noRoleMenus) {
324 embed.setDescription("No role menu page have been set up yet. Use the button below to add one.\n\n" +
325 createPageIndicator(1, 1, undefined, true)
326 );
327 pageSelect.setDisabled(true);
328 actionSelect.setDisabled(true);
329 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
330 .setLabel("No role menu pages")
331 .setValue("none")
332 );
333 } else {
334 page = Math.min(page, Object.keys(currentObject).length - 1);
335 current = currentObject[page]!;
336 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
337 `**Description:** \`${current.description}\`\n` +
338 `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}`
339 );
340
341 pageSelect.addOptions(
342 currentObject.map((key: ObjectSchema, index) => {
343 return new StringSelectMenuOptionBuilder()
344 .setLabel(ellipsis(key.name, 50))
345 .setDescription(ellipsis(key.description, 50))
346 .setValue(index.toString());
347 })
348 );
349
350 }
351
352 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
353 let i: StringSelectMenuInteraction | ButtonInteraction;
354 try {
355 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;
356 } catch (e) {
357 closed = true;
358 break;
359 }
360
361 await i.deferUpdate();
362 if (i.isButton()) {
363 switch (i.customId) {
364 case "back":
365 page--;
366 break;
367 case "next":
368 page++;
369 break;
370 case "add":
TheCodedProff4facde2023-01-28 13:42:48 -0500371 let newPage = await createRoleMenuPage(i, m)
372 if(!newPage) break;
373 currentObject.push();
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500374 page = currentObject.length - 1;
375 break;
376 case "reorder":
377 break;
378 case "save":
379 client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject});
380 modified = false;
381 break;
382 }
383 } else if (i.isStringSelectMenu()) {
384 switch (i.customId) {
385 case "action":
386 switch(i.values[0]) {
387 case "edit":
TheCodedProff4facde2023-01-28 13:42:48 -0500388 let edited = await createRoleMenuPage(i, m, current!);
389 if(!edited) break;
390 currentObject[page] = edited;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500391 modified = true;
392 break;
393 case "delete":
TheCodedProff4facde2023-01-28 13:42:48 -0500394 if(page === 0 && currentObject.keys.length - 1 > 0) page++;
395 else page--;
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500396 currentObject.splice(page, 1);
397 break;
398 }
399 break;
400 case "page":
401 page = parseInt(i.values[0]!);
402 break;
403 }
404 }
405
406 } while (!closed)
pineafan63fc5e22022-08-04 22:04:10 +0100407};
pineafanda6e5342022-07-03 10:03:16 +0100408
TheCodedProff86ba092023-01-27 17:10:07 -0500409const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100410 const member = interaction.member as Discord.GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000411 if (!member.permissions.has("ManageRoles"))
412 return "You must have the *Manage Roles* permission to use this command";
pineafanda6e5342022-07-03 10:03:16 +0100413 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100414};
pineafanda6e5342022-07-03 10:03:16 +0100415
416export { command };
417export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100418export { check };