blob: 2cdd1f83f9f6e2bef4dc4d488f97f57a49aeeea1 [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";
pineafanda6e5342022-07-03 10:03:16 +010010
11const command = (builder: SlashCommandSubcommandBuilder) =>
12 builder
pineafan63fc5e22022-08-04 22:04:10 +010013 .setName("rolemenu")
TheCodedProf1c3ad3c2023-01-25 17:58:36 -050014 .setDescription("rolemenu")
15
16interface ObjectSchema {
17 name: string;
18 description: string;
19 min: number;
20 max: number;
21 options: {
22 name: string;
23 description: string | null;
24 role: string;
25 }[];
26}
27
28const editNameDescription = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data: {name?: string, description?: string}) => {
29
30 let {name, description} = data;
31 const modal = new ModalBuilder()
32 .setTitle("Edit Name and Description")
33 .setCustomId("editNameDescription")
34 .addComponents(
35 new ActionRowBuilder<TextInputBuilder>()
36 .addComponents(
37 new TextInputBuilder()
38 .setCustomId("name")
39 .setPlaceholder(name ?? "")
40 .setStyle(TextInputStyle.Short)
41 .setRequired(true),
42 new TextInputBuilder()
43 .setCustomId("description")
44 .setPlaceholder(description ?? "")
45 .setStyle(TextInputStyle.Short)
46 )
47 )
48 const button = new ActionRowBuilder<ButtonBuilder>()
49 .addComponents(
50 new ButtonBuilder()
51 .setCustomId("back")
52 .setLabel("Back")
53 .setStyle(ButtonStyle.Secondary)
54 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
55 )
56
57 return [name, description]
58
59}
60
61const ellipsis = (str: string, max: number): string => {
62 if (str.length <= max) return str;
63 return str.slice(0, max - 3) + "...";
64}
65
66const createRoleMenuPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: ObjectSchema) => {
67 if (!data) data = {
68 name: "Role Menu Page",
69 description: "A new role menu page",
70 min: 0,
71 max: 0,
72 options: []
73 };
74 const buttons = new ActionRowBuilder<ButtonBuilder>()
75 .addComponents(
76 new ButtonBuilder()
77 .setCustomId("back")
78 .setLabel("Back")
79 .setStyle(ButtonStyle.Secondary)
80 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
81 new ButtonBuilder()
82 .setCustomId("edit")
83 .setLabel("Edit")
84 .setStyle(ButtonStyle.Primary)
85 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
86 new ButtonBuilder()
87 .setCustomId("addRole")
88 .setLabel("Add Role")
89 .setStyle(ButtonStyle.Secondary)
90 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
91 );
92
93 let back = false
94 do {
95 const previewSelect = configToDropdown("Edit Roles", {name: data.name, description: data.description, min: 1, max: 1, options: data.options});
96 const embed = new EmojiEmbed()
97 .setTitle(`${data.name}`)
98 .setStatus("Success")
99 .setDescription(
100 `**Description:**\n> ${data.description}\n\n` +
101 `**Min:** ${data.min}` + (data.min === 0 ? " (Members will be given a skip button)" : "") + "\n" +
102 `**Max:** ${data.max}\n`
103 )
104
105 interaction.editReply({embeds: [embed], components: [previewSelect, buttons]});
106 let i: StringSelectMenuInteraction | ButtonInteraction;
107 try {
108 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;
109 } catch (e) {
110 back = true;
111 break;
112 }
113
114 if (i.isStringSelectMenu()) {
115 if(i.customId === "roles") {
116 await i.deferUpdate();
117 await createRoleMenuOptionPage(interaction, m, data.options.find((o) => o.role === (i as StringSelectMenuInteraction).values[0]));
118 }
119 } else if (i.isButton()) {
120 await i.deferUpdate();
121 switch (i.customId) {
122 case "back":
123 back = true;
124 break;
125 case "edit":
126 let [name, description] = await editNameDescription(interaction, m, data);
127 data.name = name ? name : data.name;
128 data.description = description ? description : data.description;
129 break;
130 case "addRole":
131 data.options.push(await createRoleMenuOptionPage(interaction, m));
132 break;
133 }
134 }
135
136 } while (!back);
137 return data;
138}
139
140const createRoleMenuOptionPage = async (interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, data?: {name: string; description: string | null; role: string}) => {
141 const { renderRole} = client.logger;
142 if (!data) data = {
143 name: "Role Menu Option",
144 description: null,
145 role: "No role set"
146 };
147 let back = false;
148 const buttons = new ActionRowBuilder<ButtonBuilder>()
149 .addComponents(
150 new ButtonBuilder()
151 .setCustomId("back")
152 .setLabel("Back")
153 .setStyle(ButtonStyle.Secondary)
154 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
155 new ButtonBuilder()
156 .setCustomId("edit")
157 .setLabel("Edit Details")
158 .setStyle(ButtonStyle.Primary)
159 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji)
160 );
161 do {
162 const roleSelect = new RoleSelectMenuBuilder().setCustomId("role").setPlaceholder(data.role ? "Set role to" : "Set the role");
163 const embed = new EmojiEmbed()
164 .setTitle(`${data.name ?? "New Role Menu Option"}`)
165 .setStatus("Success")
166 .setDescription(
167 `**Description:**\n> ${data.description ?? "No description set"}\n\n` +
168 `**Role:** ${renderRole((await interaction.guild!.roles.fetch(data.role))!) ?? "No role set"}\n`
169 )
170
171 interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<RoleSelectMenuBuilder>().addComponents(roleSelect), buttons]});
172
173 let i: RoleSelectMenuInteraction | ButtonInteraction;
174 try {
175 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;
176 } catch (e) {
177 back = true;
178 break;
179 }
180
181 if (i.isRoleSelectMenu()) {
182 if(i.customId === "role") {
183 await i.deferUpdate();
184 data.role = (i as RoleSelectMenuInteraction).values[0]!;
185 }
186 } else if (i.isButton()) {
187 await i.deferUpdate();
188 switch (i.customId) {
189 case "back":
190 back = true;
191 break;
192 case "edit":
193 await i.deferUpdate();
194 let [name, description] = await editNameDescription(interaction, m, data as {name: string; description: string});
195 data.name = name ? name : data.name;
196 data.description = description ? description : data.description;
197 break;
198 }
199 }
200 } while (!back);
201 return data;
202}
pineafanda6e5342022-07-03 10:03:16 +0100203
pineafan63fc5e22022-08-04 22:04:10 +0100204const callback = async (interaction: CommandInteraction): Promise<void> => {
TheCodedProf1c3ad3c2023-01-25 17:58:36 -0500205 if (!interaction.guild) return;
206 const m = await interaction.reply({embeds: LoadingEmbed, ephemeral: true, fetchReply: true});
207
208 let page = 0;
209 let closed = false;
210 const config = await client.database.guilds.read(interaction.guild.id);
211 let currentObject: ObjectSchema[] = config.roleMenu.options;
212 let modified = false;
213 do {
214 const embed = new EmojiEmbed()
215 .setTitle("Role Menu Settings")
216 .setEmoji("GUILD.GREEN")
217 .setStatus("Success");
218 const noRoleMenus = currentObject.length === 0;
219 let current: ObjectSchema;
220
221 const pageSelect = new StringSelectMenuBuilder()
222 .setCustomId("page")
223 .setPlaceholder("Select a Role Menu page to manage");
224 const actionSelect = new StringSelectMenuBuilder()
225 .setCustomId("action")
226 .setPlaceholder("Perform an action")
227 .addOptions(
228 new StringSelectMenuOptionBuilder()
229 .setLabel("Edit")
230 .setDescription("Edit this page")
231 .setValue("edit")
232 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
233 new StringSelectMenuOptionBuilder()
234 .setLabel("Delete")
235 .setDescription("Delete this page")
236 .setValue("delete")
237 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
238 );
239 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
240 .addComponents(
241 new ButtonBuilder()
242 .setCustomId("back")
243 .setStyle(ButtonStyle.Primary)
244 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
245 .setDisabled(page === 0),
246 new ButtonBuilder()
247 .setCustomId("next")
248 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
249 .setStyle(ButtonStyle.Primary)
250 .setDisabled(page === Object.keys(currentObject).length - 1),
251 new ButtonBuilder()
252 .setCustomId("add")
253 .setLabel("New Page")
254 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
255 .setStyle(ButtonStyle.Secondary)
256 .setDisabled(Object.keys(currentObject).length >= 24),
257 new ButtonBuilder()
258 .setCustomId("reorder")
259 .setLabel("Reorder Pages")
260 .setEmoji(getEmojiByName("ICONS.SHUFFLE", "id") as APIMessageComponentEmoji)
261 .setStyle(ButtonStyle.Secondary)
262 .setDisabled(Object.keys(currentObject).length <= 1),
263 new ButtonBuilder()
264 .setCustomId("save")
265 .setLabel("Save")
266 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
267 .setStyle(ButtonStyle.Success)
268 .setDisabled(!modified),
269 );
270 if(noRoleMenus) {
271 embed.setDescription("No role menu page have been set up yet. Use the button below to add one.\n\n" +
272 createPageIndicator(1, 1, undefined, true)
273 );
274 pageSelect.setDisabled(true);
275 actionSelect.setDisabled(true);
276 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
277 .setLabel("No role menu pages")
278 .setValue("none")
279 );
280 } else {
281 page = Math.min(page, Object.keys(currentObject).length - 1);
282 current = currentObject[page]!;
283 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
284 `**Description:** \`${current.description}\`\n` +
285 `\n\n${createPageIndicator(Object.keys(config.roleMenu.options).length, page)}`
286 );
287
288 pageSelect.addOptions(
289 currentObject.map((key: ObjectSchema, index) => {
290 return new StringSelectMenuOptionBuilder()
291 .setLabel(ellipsis(key.name, 50))
292 .setDescription(ellipsis(key.description, 50))
293 .setValue(index.toString());
294 })
295 );
296
297 }
298
299 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
300 let i: StringSelectMenuInteraction | ButtonInteraction;
301 try {
302 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;
303 } catch (e) {
304 closed = true;
305 break;
306 }
307
308 await i.deferUpdate();
309 if (i.isButton()) {
310 switch (i.customId) {
311 case "back":
312 page--;
313 break;
314 case "next":
315 page++;
316 break;
317 case "add":
318 currentObject.push(await createRoleMenuPage(i, m));
319 page = currentObject.length - 1;
320 break;
321 case "reorder":
322 break;
323 case "save":
324 client.database.guilds.write(interaction.guild.id, {"roleMenu.options": currentObject});
325 modified = false;
326 break;
327 }
328 } else if (i.isStringSelectMenu()) {
329 switch (i.customId) {
330 case "action":
331 switch(i.values[0]) {
332 case "edit":
333 currentObject[page] = await createRoleMenuPage(i, m, current!);
334 modified = true;
335 break;
336 case "delete":
337 currentObject.splice(page, 1);
338 break;
339 }
340 break;
341 case "page":
342 page = parseInt(i.values[0]!);
343 break;
344 }
345 }
346
347 } while (!closed)
pineafan63fc5e22022-08-04 22:04:10 +0100348};
pineafanda6e5342022-07-03 10:03:16 +0100349
TheCodedProff86ba092023-01-27 17:10:07 -0500350const check = (interaction: CommandInteraction, _partial: boolean = false) => {
Skyler Grey75ea9172022-08-06 10:22:23 +0100351 const member = interaction.member as Discord.GuildMember;
PineaFan0d06edc2023-01-17 22:10:31 +0000352 if (!member.permissions.has("ManageRoles"))
353 return "You must have the *Manage Roles* permission to use this command";
pineafanda6e5342022-07-03 10:03:16 +0100354 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100355};
pineafanda6e5342022-07-03 10:03:16 +0100356
357export { command };
358export { callback };
Skyler Grey75ea9172022-08-06 10:22:23 +0100359export { check };