blob: d9d485d37f5ea4c4be527a98d9daf348c89a2703 [file] [log] [blame]
TheCodedProf4f79da12023-01-31 16:50:37 -05001import { ActionRowBuilder, APIMessageComponentEmoji, ButtonBuilder, ButtonInteraction, ButtonStyle, Collection, CommandInteraction, GuildMember, Message, ModalBuilder, ModalSubmitInteraction, PermissionsBitField, Role, RoleSelectMenuBuilder, RoleSelectMenuInteraction, SlashCommandSubcommandBuilder, StringSelectMenuBuilder, StringSelectMenuInteraction, StringSelectMenuOptionBuilder, TextInputBuilder, TextInputStyle } from "discord.js";
TheCodedProfa112f612023-01-28 18:06:45 -05002import client from "../../utils/client.js";
TheCodedProfb5e9d552023-01-29 15:43:26 -05003import createPageIndicator, { createVerticalTrack } from "../../utils/createPageIndicator.js";
4import { LoadingEmbed } from "../../utils/defaults.js";
5import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
6import getEmojiByName from "../../utils/getEmojiByName.js";
7import ellipsis from "../../utils/ellipsis.js";
8import { modalInteractionCollector } from "../../utils/dualCollector.js";
TheCodedProfa112f612023-01-28 18:06:45 -05009
TheCodedProf4f79da12023-01-31 16:50:37 -050010const { renderRole } = client.logger
11
TheCodedProfa112f612023-01-28 18:06:45 -050012const command = (builder: SlashCommandSubcommandBuilder) =>
13 builder
14 .setName("tracks")
15 .setDescription("Manage the tracks for the server")
16
TheCodedProfb5e9d552023-01-29 15:43:26 -050017interface ObjectSchema {
18 name: string;
19 retainPrevious: boolean;
20 nullable: boolean;
21 track: string[];
22 manageableBy: string[];
23}
24
TheCodedProf4f79da12023-01-31 16:50:37 -050025
TheCodedProfb5e9d552023-01-29 15:43:26 -050026const editName = async (i: ButtonInteraction, interaction: StringSelectMenuInteraction | ButtonInteraction, m: Message, current?: string) => {
27
28 let name = current ?? "";
29 const modal = new ModalBuilder()
30 .setTitle("Edit Name and Description")
31 .setCustomId("editNameDescription")
32 .addComponents(
33 new ActionRowBuilder<TextInputBuilder>()
34 .addComponents(
35 new TextInputBuilder()
36 .setLabel("Name")
37 .setCustomId("name")
TheCodedProf1f675042023-02-16 17:01:29 -050038 .setPlaceholder("The name of the track (e.g. Moderators)")
TheCodedProfb5e9d552023-01-29 15:43:26 -050039 .setStyle(TextInputStyle.Short)
PineaFanb0d0c242023-02-05 10:59:45 +000040 .setValue(name)
TheCodedProfb5e9d552023-01-29 15:43:26 -050041 .setRequired(true)
42 )
43 )
44 const button = new ActionRowBuilder<ButtonBuilder>()
45 .addComponents(
46 new ButtonBuilder()
47 .setCustomId("back")
48 .setLabel("Back")
49 .setStyle(ButtonStyle.Secondary)
50 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
51 )
52
53 await i.showModal(modal)
54 await interaction.editReply({
55 embeds: [
56 new EmojiEmbed()
57 .setTitle("Tracks")
58 .setDescription("Modal opened. If you can't see it, click back and try again.")
59 .setStatus("Success")
60 ],
61 components: [button]
62 });
63
64 let out: ModalSubmitInteraction | null;
65 try {
TheCodedProf01cba762023-02-18 15:55:05 -050066 out = await modalInteractionCollector(m, interaction.user) as ModalSubmitInteraction | null;
TheCodedProfb5e9d552023-01-29 15:43:26 -050067 } catch (e) {
68 console.error(e);
69 out = null;
70 }
71 if(!out) return name;
72 if (out.isButton()) return name;
TheCodedProfb5e9d552023-01-29 15:43:26 -050073 name = out.fields.fields.find((f) => f.customId === "name")?.value ?? name;
74 return name
75
76}
77
78const reorderTracks = async (interaction: ButtonInteraction, m: Message, roles: Collection<string, Role>, currentObj: string[]) => {
PineaFanb0d0c242023-02-05 10:59:45 +000079 const reorderRow = new ActionRowBuilder<StringSelectMenuBuilder>()
TheCodedProfb5e9d552023-01-29 15:43:26 -050080 .addComponents(
81 new StringSelectMenuBuilder()
82 .setCustomId("reorder")
83 .setPlaceholder("Select all roles in the order you want users to gain them (Lowest to highest rank).")
84 .setMinValues(currentObj.length)
85 .setMaxValues(currentObj.length)
86 .addOptions(
87 currentObj.map((o, i) => new StringSelectMenuOptionBuilder()
88 .setLabel(roles.get(o)!.name)
89 .setValue(i.toString())
90 )
91 )
92 );
PineaFanb0d0c242023-02-05 10:59:45 +000093 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
TheCodedProfb5e9d552023-01-29 15:43:26 -050094 .addComponents(
95 new ButtonBuilder()
96 .setCustomId("back")
97 .setLabel("Back")
98 .setStyle(ButtonStyle.Secondary)
99 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
100 )
101 await interaction.editReply({
102 embeds: [
103 new EmojiEmbed()
104 .setTitle("Tracks")
105 .setDescription("Select all roles in the order you want users to gain them (Lowest to highest rank).")
106 .setStatus("Success")
107 ],
108 components: [reorderRow, buttonRow]
109 });
110 let out: StringSelectMenuInteraction | ButtonInteraction | null;
111 try {
112 out = await m.awaitMessageComponent({
113 filter: (i) => i.channel!.id === interaction.channel!.id,
114 time: 300000
115 }) as StringSelectMenuInteraction | ButtonInteraction | null;
116 } catch (e) {
117 console.error(e);
118 out = null;
119 }
120 if(!out) return;
121 out.deferUpdate();
122 if (out.isButton()) return;
TheCodedProfb5e9d552023-01-29 15:43:26 -0500123 const values = out.values;
124
125 const newOrder: string[] = currentObj.map((_, i) => {
126 const index = values.findIndex(v => v === i.toString());
127 return currentObj[index];
128 }) as string[];
129
130 return newOrder;
131}
132
133const editTrack = async (interaction: ButtonInteraction | StringSelectMenuInteraction, message: Message, roles: Collection<string, Role>, current?: ObjectSchema) => {
TheCodedProf4f79da12023-01-31 16:50:37 -0500134 const isAdmin = (interaction.member!.permissions as PermissionsBitField).has("Administrator");
TheCodedProfb5e9d552023-01-29 15:43:26 -0500135 if(!current) {
136 current = {
137 name: "",
138 retainPrevious: false,
139 nullable: false,
140 track: [],
141 manageableBy: []
142 }
143 }
TheCodedProf4f79da12023-01-31 16:50:37 -0500144
TheCodedProfb5e9d552023-01-29 15:43:26 -0500145 const roleSelect = new ActionRowBuilder<RoleSelectMenuBuilder>()
146 .addComponents(
147 new RoleSelectMenuBuilder()
148 .setCustomId("addRole")
149 .setPlaceholder("Select a role to add")
TheCodedProf4f79da12023-01-31 16:50:37 -0500150 .setDisabled(!isAdmin)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500151 );
152 let closed = false;
153 do {
154 const editableRoles: string[] = current.track.map((r) => {
TheCodedProfd0a166d2023-02-19 00:04:53 -0500155 if(!(roles.get(r)!.position >= (interaction.member as GuildMember).roles.highest.position) || interaction.user.id === interaction.guild?.ownerId) return roles.get(r)!.name;
TheCodedProfb5e9d552023-01-29 15:43:26 -0500156 }).filter(v => v !== undefined) as string[];
157 const selectMenu = new ActionRowBuilder<StringSelectMenuBuilder>()
158 .addComponents(
159 new StringSelectMenuBuilder()
160 .setCustomId("removeRole")
161 .setPlaceholder("Select a role to remove")
TheCodedProf4f79da12023-01-31 16:50:37 -0500162 .setDisabled(!isAdmin)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500163 .addOptions(
164 editableRoles.map((r, i) => {
165 return new StringSelectMenuOptionBuilder()
166 .setLabel(r)
167 .setValue(i.toString())}
168 )
169 )
170 );
TheCodedProf4f79da12023-01-31 16:50:37 -0500171 const buttons = new ActionRowBuilder<ButtonBuilder>()
172 .addComponents(
173 new ButtonBuilder()
174 .setCustomId("back")
175 .setLabel("Back")
176 .setStyle(ButtonStyle.Secondary)
177 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji),
178 new ButtonBuilder()
179 .setCustomId("edit")
180 .setLabel("Edit Name")
181 .setStyle(ButtonStyle.Primary)
182 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
183 new ButtonBuilder()
184 .setCustomId("reorder")
185 .setLabel("Reorder")
186 .setDisabled(!isAdmin)
187 .setStyle(ButtonStyle.Primary)
188 .setEmoji(getEmojiByName("ICONS.REORDER", "id") as APIMessageComponentEmoji),
189 new ButtonBuilder()
190 .setCustomId("retainPrevious")
191 .setLabel("Retain Previous")
192 .setStyle(current.retainPrevious ? ButtonStyle.Success : ButtonStyle.Danger)
193 .setEmoji(getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji),
194 new ButtonBuilder()
195 .setCustomId("nullable")
196 .setLabel(`Role ${current.nullable ? "Not " : ""}Required`)
197 .setStyle(current.nullable ? ButtonStyle.Success : ButtonStyle.Danger)
198 .setEmoji(getEmojiByName("CONTROL." + (current.nullable ? "TICK" : "CROSS"), "id") as APIMessageComponentEmoji)
199 );
200
PineaFanb0d0c242023-02-05 10:59:45 +0000201 const allowed: boolean[] = [];
TheCodedProfb5e9d552023-01-29 15:43:26 -0500202 for (const role of current.track) {
203 const disabled: boolean =
204 roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position;
205 allowed.push(disabled)
206 }
TheCodedProf4f79da12023-01-31 16:50:37 -0500207 const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500208
209 const embed = new EmojiEmbed()
210 .setTitle("Tracks")
211 .setDescription(
212 `**Currently Editing:** ${current.name}\n\n` +
TheCodedProf4f79da12023-01-31 16:50:37 -0500213 `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` +
214 `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` +
215 createVerticalTrack(
216 mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false), allowed)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500217 )
TheCodedProf4f79da12023-01-31 16:50:37 -0500218 .setStatus("Success")
TheCodedProfb5e9d552023-01-29 15:43:26 -0500219
TheCodedProf1807fb32023-02-20 14:33:48 -0500220 const comps: ActionRowBuilder<RoleSelectMenuBuilder | ButtonBuilder | StringSelectMenuBuilder>[] = [roleSelect, buttons];
TheCodedProfd0a166d2023-02-19 00:04:53 -0500221 if(current.track.length >= 1) comps.splice(1, 0, selectMenu);
222
223 interaction.editReply({embeds: [embed], components: comps});
TheCodedProfb5e9d552023-01-29 15:43:26 -0500224
225 let out: ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null;
226
227 try {
228 out = await message.awaitMessageComponent({
229 filter: (i) => i.channel!.id === interaction.channel!.id,
230 time: 300000
231 }) as ButtonInteraction | RoleSelectMenuInteraction | StringSelectMenuInteraction | null;
232 } catch (e) {
233 console.error(e);
234 out = null;
235 }
236
237 if(!out) return;
238 if (out.isButton()) {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500239 switch(out.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000240 case "back": {
TheCodedProfd0a166d2023-02-19 00:04:53 -0500241 out.deferUpdate();
TheCodedProfb5e9d552023-01-29 15:43:26 -0500242 closed = true;
243 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000244 }
245 case "edit": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500246 current.name = (await editName(out, interaction, message, current.name))!;
247 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000248 }
249 case "reorder": {
TheCodedProfd0a166d2023-02-19 00:04:53 -0500250 out.deferUpdate();
TheCodedProfb5e9d552023-01-29 15:43:26 -0500251 current.track = (await reorderTracks(out, message, roles, current.track))!;
TheCodedProf4f79da12023-01-31 16:50:37 -0500252 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000253 }
254 case "retainPrevious": {
TheCodedProfd0a166d2023-02-19 00:04:53 -0500255 out.deferUpdate();
TheCodedProf4f79da12023-01-31 16:50:37 -0500256 current.retainPrevious = !current.retainPrevious;
257 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000258 }
259 case "nullable": {
TheCodedProfd0a166d2023-02-19 00:04:53 -0500260 out.deferUpdate();
TheCodedProf4f79da12023-01-31 16:50:37 -0500261 current.nullable = !current.nullable;
262 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000263 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500264 }
265 } else if (out.isStringSelectMenu()) {
266 out.deferUpdate();
267 switch(out.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000268 case "removeRole": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500269 const index = current.track.findIndex(v => v === editableRoles[parseInt((out! as StringSelectMenuInteraction).values![0]!)]);
270 current.track.splice(index, 1);
271 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000272 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500273 }
274 } else {
275 switch(out.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000276 case "addRole": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500277 const role = out.values![0]!;
278 if(!current.track.includes(role)) {
279 current.track.push(role);
TheCodedProfd0a166d2023-02-19 00:04:53 -0500280 } else {
281 out.reply({content: "That role is already on this track", ephemeral: true})
TheCodedProfb5e9d552023-01-29 15:43:26 -0500282 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500283 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000284 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500285 }
286 }
287
288 } while(!closed);
289 return current;
290}
TheCodedProfa112f612023-01-28 18:06:45 -0500291
292const callback = async (interaction: CommandInteraction) => {
293
TheCodedProfb5e9d552023-01-29 15:43:26 -0500294 const m = await interaction.reply({embeds: LoadingEmbed, fetchReply: true, ephemeral: true})
295 const config = await client.database.guilds.read(interaction.guild!.id);
296 const tracks: ObjectSchema[] = config.tracks;
297 const roles = await interaction.guild!.roles.fetch();
TheCodedProfb5e9d552023-01-29 15:43:26 -0500298
299 let page = 0;
300 let closed = false;
301 let modified = false;
302
303 do {
304 const embed = new EmojiEmbed()
305 .setTitle("Track Settings")
306 .setEmoji("TRACKS.ICON")
307 .setStatus("Success");
308 const noTracks = config.tracks.length === 0;
309 let current: ObjectSchema;
310
311 const pageSelect = new StringSelectMenuBuilder()
312 .setCustomId("page")
313 .setPlaceholder("Select a track to manage");
314 const actionSelect = new StringSelectMenuBuilder()
315 .setCustomId("action")
316 .setPlaceholder("Perform an action")
317 .addOptions(
318 new StringSelectMenuOptionBuilder()
319 .setLabel("Edit")
320 .setDescription("Edit this track")
321 .setValue("edit")
322 .setEmoji(getEmojiByName("ICONS.EDIT", "id") as APIMessageComponentEmoji),
323 new StringSelectMenuOptionBuilder()
324 .setLabel("Delete")
325 .setDescription("Delete this track")
326 .setValue("delete")
327 .setEmoji(getEmojiByName("TICKETS.ISSUE", "id") as APIMessageComponentEmoji)
328 );
329 const buttonRow = new ActionRowBuilder<ButtonBuilder>()
330 .addComponents(
331 new ButtonBuilder()
332 .setCustomId("back")
333 .setStyle(ButtonStyle.Primary)
334 .setEmoji(getEmojiByName("CONTROL.LEFT", "id") as APIMessageComponentEmoji)
335 .setDisabled(page === 0),
336 new ButtonBuilder()
337 .setCustomId("next")
338 .setEmoji(getEmojiByName("CONTROL.RIGHT", "id") as APIMessageComponentEmoji)
339 .setStyle(ButtonStyle.Primary)
TheCodedProfd0a166d2023-02-19 00:04:53 -0500340 .setDisabled(page === tracks.length - 1),
TheCodedProfb5e9d552023-01-29 15:43:26 -0500341 new ButtonBuilder()
342 .setCustomId("add")
343 .setLabel("New Track")
344 .setEmoji(getEmojiByName("TICKETS.SUGGESTION", "id") as APIMessageComponentEmoji)
345 .setStyle(ButtonStyle.Secondary)
346 .setDisabled(Object.keys(tracks).length >= 24),
347 new ButtonBuilder()
348 .setCustomId("save")
349 .setLabel("Save")
350 .setEmoji(getEmojiByName("ICONS.SAVE", "id") as APIMessageComponentEmoji)
351 .setStyle(ButtonStyle.Success)
352 .setDisabled(!modified),
353 );
354 if(noTracks) {
355 embed.setDescription("No tracks have been set up yet. Use the button below to add one.\n\n" +
356 createPageIndicator(1, 1, undefined, true)
357 );
358 pageSelect.setDisabled(true);
359 actionSelect.setDisabled(true);
360 pageSelect.addOptions(new StringSelectMenuOptionBuilder()
361 .setLabel("No tracks")
362 .setValue("none")
363 );
364 } else {
365 page = Math.min(page, Object.keys(tracks).length - 1);
366 current = tracks[page]!;
TheCodedProf4f79da12023-01-31 16:50:37 -0500367 const mapped = current.track.map(role => roles.find(aRole => aRole.id === role)!);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500368 embed.setDescription(`**Currently Editing:** ${current.name}\n\n` +
369 `${getEmojiByName("CONTROL." + (current.nullable ? "CROSS" : "TICK"))} Members ${current.nullable ? "don't " : ""}need a role in this track\n` +
370 `${getEmojiByName("CONTROL." + (current.retainPrevious ? "TICK" : "CROSS"))} Members ${current.retainPrevious ? "" : "don't "}keep all roles below their current highest\n\n` +
TheCodedProf4f79da12023-01-31 16:50:37 -0500371 createVerticalTrack(mapped.map(role => renderRole(role)), new Array(current.track.length).fill(false)) +
TheCodedProfb5e9d552023-01-29 15:43:26 -0500372 `\n${createPageIndicator(config.tracks.length, page)}`
373 );
374
375 pageSelect.addOptions(
376 tracks.map((key: ObjectSchema, index) => {
377 return new StringSelectMenuOptionBuilder()
378 .setLabel(ellipsis(key.name, 50))
379 .setDescription(ellipsis(roles.get(key.track[0]!)?.name!, 50))
380 .setValue(index.toString());
381 })
382 );
383
384 }
385
386 await interaction.editReply({embeds: [embed], components: [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(actionSelect), new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(pageSelect), buttonRow]});
387 let i: StringSelectMenuInteraction | ButtonInteraction;
388 try {
389 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;
390 } catch (e) {
391 closed = true;
PineaFanb0d0c242023-02-05 10:59:45 +0000392 continue;
TheCodedProfb5e9d552023-01-29 15:43:26 -0500393 }
394
395 await i.deferUpdate();
396 if (i.isButton()) {
397 switch (i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000398 case "back": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500399 page--;
400 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000401 }
402 case "next": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500403 page++;
404 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000405 }
406 case "add": {
407 const newPage = await editTrack(i, m, roles)
TheCodedProfb5e9d552023-01-29 15:43:26 -0500408 if(!newPage) break;
409 tracks.push();
410 page = tracks.length - 1;
411 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000412 }
413 case "save": {
TheCodedProf4f79da12023-01-31 16:50:37 -0500414 client.database.guilds.write(interaction.guild!.id, {tracks: tracks});
TheCodedProfb5e9d552023-01-29 15:43:26 -0500415 modified = false;
416 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000417 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500418 }
419 } else if (i.isStringSelectMenu()) {
420 switch (i.customId) {
PineaFanb0d0c242023-02-05 10:59:45 +0000421 case "action": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500422 switch(i.values[0]) {
PineaFanb0d0c242023-02-05 10:59:45 +0000423 case "edit": {
424 const edited = await editTrack(i, m, roles, current!);
TheCodedProfb5e9d552023-01-29 15:43:26 -0500425 if(!edited) break;
426 tracks[page] = edited;
427 modified = true;
428 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000429 }
430 case "delete": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500431 if(page === 0 && tracks.keys.length - 1 > 0) page++;
432 else page--;
433 tracks.splice(page, 1);
434 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000435 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500436 }
437 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000438 }
439 case "page": {
TheCodedProfb5e9d552023-01-29 15:43:26 -0500440 page = parseInt(i.values[0]!);
441 break;
PineaFanb0d0c242023-02-05 10:59:45 +0000442 }
TheCodedProfb5e9d552023-01-29 15:43:26 -0500443 }
444 }
445
TheCodedProf01cba762023-02-18 15:55:05 -0500446 } while (!closed);
447 await interaction.deleteReply()
TheCodedProfa112f612023-01-28 18:06:45 -0500448}
449
450const check = (interaction: CommandInteraction, _partial: boolean = false) => {
451 const member = interaction.member as GuildMember;
452 if (!member.permissions.has("ManageRoles"))
453 return "You must have the *Manage Server* permission to use this command";
454 return true;
455};
456
457export { command };
458export { callback };
459export { check };