blob: 25a784bddead153a885fa4a56b12cf36faaf2e02 [file] [log] [blame]
PineaFan0d06edc2023-01-17 22:10:31 +00001import { LoadingEmbed } from "../../utils/defaults.js";
PineaFan638eb132023-01-19 10:41:22 +00002import Discord, { CommandInteraction, GuildMember, Message, ActionRowBuilder, ButtonBuilder, ButtonStyle, SelectMenuOptionBuilder, APIMessageComponentEmoji, StringSelectMenuBuilder, MessageComponentInteraction, StringSelectMenuInteraction } from "discord.js";
TheCodedProff86ba092023-01-27 17:10:07 -05003import type { SlashCommandSubcommandBuilder } from "discord.js";
pineafan4edb7762022-06-26 19:21:04 +01004import EmojiEmbed from "../../utils/generateEmojiEmbed.js";
pineafanad54d752022-04-18 19:01:43 +01005import getEmojiByName from "../../utils/getEmojiByName.js";
pineafane625d782022-05-09 18:04:32 +01006import addPlural from "../../utils/plurals.js";
pineafan6702cef2022-06-13 17:52:37 +01007import client from "../../utils/client.js";
pineafanad54d752022-04-18 19:01:43 +01008
9const command = (builder: SlashCommandSubcommandBuilder) =>
10 builder
pineafan63fc5e22022-08-04 22:04:10 +010011 .setName("track")
12 .setDescription("Moves a user along a role track")
Skyler Grey11236ba2022-08-08 21:13:33 +010013 .addUserOption((option) => option.setName("user").setDescription("The user to manage").setRequired(true));
pineafanad54d752022-04-18 19:01:43 +010014
Skyler Grey11236ba2022-08-08 21:13:33 +010015const generateFromTrack = (position: number, active: string | boolean, size: number, disabled: string | boolean) => {
pineafan63fc5e22022-08-04 22:04:10 +010016 active = active ? "ACTIVE" : "INACTIVE";
17 disabled = disabled ? "GREY." : "";
Skyler Grey11236ba2022-08-08 21:13:33 +010018 if (position === 0 && size === 1) return "TRACKS.SINGLE." + disabled + active;
19 if (position === size - 1) return "TRACKS.VERTICAL.BOTTOM." + disabled + active;
pineafan63fc5e22022-08-04 22:04:10 +010020 if (position === 0) return "TRACKS.VERTICAL.TOP." + disabled + active;
21 return "TRACKS.VERTICAL.MIDDLE." + disabled + active;
22};
pineafanad54d752022-04-18 19:01:43 +010023
Skyler Greyc634e2b2022-08-06 17:50:48 +010024const callback = async (interaction: CommandInteraction): Promise<unknown> => {
PineappleFanb3dd83c2022-06-17 10:53:48 +010025 const { renderUser } = client.logger;
pineafanad54d752022-04-18 19:01:43 +010026 const member = interaction.options.getMember("user") as GuildMember;
27 const guild = interaction.guild;
pineafan63fc5e22022-08-04 22:04:10 +010028 if (!guild) return;
29 const config = await client.database.guilds.read(guild.id);
Skyler Grey75ea9172022-08-06 10:22:23 +010030 await interaction.reply({ embeds: LoadingEmbed, ephemeral: true });
pineafan63fc5e22022-08-04 22:04:10 +010031 let track = 0;
pineafanad54d752022-04-18 19:01:43 +010032 let generated;
pineafan63fc5e22022-08-04 22:04:10 +010033 const roles = await guild.roles.fetch();
Skyler Greyc634e2b2022-08-06 17:50:48 +010034 const memberRoles = member.roles;
pineafan63fc5e22022-08-04 22:04:10 +010035 let managed: boolean;
Skyler Greyad002172022-08-16 18:48:26 +010036 let timedOut = false;
37 while (!timedOut) {
TheCodedProf2ec2ba62023-01-18 22:26:20 -050038 const data = config.tracks[track]!;
PineaFan638eb132023-01-19 10:41:22 +000039 if (data.manageableBy.length)
Skyler Grey75ea9172022-08-06 10:22:23 +010040 managed = data.manageableBy.some((element: string) => {
41 return memberRoles.cache.has(element);
pineafan63fc5e22022-08-04 22:04:10 +010042 });
Skyler Grey75ea9172022-08-06 10:22:23 +010043 else managed = false;
TheCodedProf2ec2ba62023-01-18 22:26:20 -050044 const dropdown = new Discord.StringSelectMenuBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +010045 .addOptions(
46 config.tracks.map((option, index) => {
Skyler Grey11236ba2022-08-08 21:13:33 +010047 const hasRoleInTrack = option.track.some((element: string) => {
48 return memberRoles.cache.has(element);
49 });
TheCodedProf2ec2ba62023-01-18 22:26:20 -050050 return new SelectMenuOptionBuilder({
Skyler Grey75ea9172022-08-06 10:22:23 +010051 default: index === track,
52 label: option.name,
53 value: index.toString(),
Skyler Grey11236ba2022-08-08 21:13:33 +010054 description: option.track.length === 0 ? "No" : addPlural(option.track.length, "role"),
Skyler Grey75ea9172022-08-06 10:22:23 +010055 emoji: client.emojis.resolve(
Skyler Grey11236ba2022-08-08 21:13:33 +010056 getEmojiByName("TRACKS.SINGLE." + (hasRoleInTrack ? "ACTIVE" : "INACTIVE"), "id")
TheCodedProf2ec2ba62023-01-18 22:26:20 -050057 ) as APIMessageComponentEmoji
Skyler Grey75ea9172022-08-06 10:22:23 +010058 });
59 })
60 )
61 .setCustomId("select")
62 .setMaxValues(1);
TheCodedProf2ec2ba62023-01-18 22:26:20 -050063 const allowed: boolean[] = [];
Skyler Grey11236ba2022-08-08 21:13:33 +010064 generated = "**Track:** " + data.name + "\n" + "**Member:** " + renderUser(member.user) + "\n";
Skyler Grey75ea9172022-08-06 10:22:23 +010065 generated +=
Skyler Grey11236ba2022-08-08 21:13:33 +010066 (data.nullable ? "Members do not need a role in this track" : "A role in this track is required") + "\n";
Skyler Grey75ea9172022-08-06 10:22:23 +010067 generated +=
68 (data.retainPrevious
69 ? "When promoted, the user keeps previous roles"
70 : "Members will lose their current role when promoted") + "\n";
71 generated +=
72 "\n" +
73 data.track
74 .map((role, index) => {
TheCodedProf2ec2ba62023-01-18 22:26:20 -050075 const allow: boolean =
76 roles.get(role)!.position >= (interaction.member as GuildMember).roles.highest.position &&
Skyler Grey11236ba2022-08-08 21:13:33 +010077 !managed;
Skyler Grey75ea9172022-08-06 10:22:23 +010078 allowed.push(!allow);
79 return (
80 getEmojiByName(
Skyler Grey11236ba2022-08-08 21:13:33 +010081 generateFromTrack(index, memberRoles.cache.has(role), data.track.length, allow)
Skyler Grey75ea9172022-08-06 10:22:23 +010082 ) +
83 " " +
TheCodedProf2ec2ba62023-01-18 22:26:20 -050084 roles.get(role)!.name +
Skyler Grey75ea9172022-08-06 10:22:23 +010085 " [<@&" +
TheCodedProf2ec2ba62023-01-18 22:26:20 -050086 roles.get(role)!.id +
Skyler Grey75ea9172022-08-06 10:22:23 +010087 ">]"
88 );
89 })
90 .join("\n");
pineafan63fc5e22022-08-04 22:04:10 +010091 const selected = [];
Skyler Greyc634e2b2022-08-06 17:50:48 +010092 for (const position of data.track) {
93 if (memberRoles.cache.has(position)) selected.push(position);
pineafanad54d752022-04-18 19:01:43 +010094 }
pineafan63fc5e22022-08-04 22:04:10 +010095 const conflict = data.retainPrevious ? false : selected.length > 1;
TheCodedProf2ec2ba62023-01-18 22:26:20 -050096 let conflictDropdown: StringSelectMenuBuilder[] = [];
PineaFan638eb132023-01-19 10:41:22 +000097 const conflictDropdownOptions: SelectMenuOptionBuilder[] = [];
TheCodedProf2ec2ba62023-01-18 22:26:20 -050098 let currentRoleIndex: number = -1;
pineafanad54d752022-04-18 19:01:43 +010099 if (conflict) {
Skyler Grey11236ba2022-08-08 21:13:33 +0100100 generated += `\n\n${getEmojiByName(`PUNISH.WARN.${managed ? "YELLOW" : "RED"}`)} This user has ${
101 selected.length
102 } roles from this track. `;
pineafan63fc5e22022-08-04 22:04:10 +0100103 conflictDropdown = [];
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500104 if (roles.get(selected[0]!)!.position < memberRoles.highest.position || managed) {
Skyler Grey75ea9172022-08-06 10:22:23 +0100105 generated +=
106 "In order to promote or demote this user, you must select which role the member should keep.";
107 selected.forEach((role) => {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500108 conflictDropdownOptions.push(
109 new SelectMenuOptionBuilder({
110 label: roles.get(role)!.name,
111 value: roles.get(role)!.id
Skyler Grey75ea9172022-08-06 10:22:23 +0100112 })
113 );
pineafan63fc5e22022-08-04 22:04:10 +0100114 });
Skyler Grey75ea9172022-08-06 10:22:23 +0100115 conflictDropdown = [
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500116 new Discord.StringSelectMenuBuilder()
117 .addOptions(conflictDropdownOptions)
Skyler Grey75ea9172022-08-06 10:22:23 +0100118 .setCustomId("conflict")
119 .setMaxValues(1)
120 .setPlaceholder("Select a role to keep")
121 ];
pineafanad54d752022-04-18 19:01:43 +0100122 } else {
Skyler Grey75ea9172022-08-06 10:22:23 +0100123 generated +=
124 "You don't have permission to manage one or more of the users roles, and therefore can't select one to keep.";
pineafanad54d752022-04-18 19:01:43 +0100125 }
126 } else {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500127 currentRoleIndex = selected.length === 0 ? -1 : data.track.indexOf(selected[0]!.toString());
pineafanad54d752022-04-18 19:01:43 +0100128 }
Skyler Grey75ea9172022-08-06 10:22:23 +0100129 const m = (await interaction.editReply({
130 embeds: [
131 new EmojiEmbed()
132 .setEmoji("TRACKS.ICON")
133 .setTitle("Tracks")
134 .setDescription(`${generated}`)
135 .setStatus("Success")
136 ],
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500137 components: [new ActionRowBuilder<StringSelectMenuBuilder | ButtonBuilder>().addComponents(dropdown)]
Skyler Grey75ea9172022-08-06 10:22:23 +0100138 .concat(
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500139 conflict && conflictDropdown.length ? [new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(conflictDropdown)] : []
Skyler Grey75ea9172022-08-06 10:22:23 +0100140 )
141 .concat([
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500142 new ActionRowBuilder<ButtonBuilder>().addComponents([
TheCodedProf21c08592022-09-13 14:14:43 -0400143 new ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +0100144 .setEmoji(getEmojiByName("CONTROL.UP", "id"))
145 .setLabel("Move up")
146 .setCustomId("promote")
TheCodedProf21c08592022-09-13 14:14:43 -0400147 .setStyle(ButtonStyle.Success)
Skyler Grey75ea9172022-08-06 10:22:23 +0100148 .setDisabled(
149 conflict ||
150 currentRoleIndex === 0 ||
Skyler Grey11236ba2022-08-08 21:13:33 +0100151 (currentRoleIndex === -1 ? false : !allowed[currentRoleIndex - 1])
Skyler Grey75ea9172022-08-06 10:22:23 +0100152 ),
TheCodedProf21c08592022-09-13 14:14:43 -0400153 new ButtonBuilder()
Skyler Grey75ea9172022-08-06 10:22:23 +0100154 .setEmoji(getEmojiByName("CONTROL.DOWN", "id"))
155 .setLabel("Move down")
156 .setCustomId("demote")
TheCodedProf21c08592022-09-13 14:14:43 -0400157 .setStyle(ButtonStyle.Danger)
Skyler Grey75ea9172022-08-06 10:22:23 +0100158 .setDisabled(
159 conflict ||
160 (data.nullable
161 ? currentRoleIndex <= -1
Skyler Grey11236ba2022-08-08 21:13:33 +0100162 : currentRoleIndex === data.track.length - 1 || currentRoleIndex <= -1) ||
Skyler Grey75ea9172022-08-06 10:22:23 +0100163 !allowed[currentRoleIndex]
164 )
165 ])
pineafan63fc5e22022-08-04 22:04:10 +0100166 ])
Skyler Grey75ea9172022-08-06 10:22:23 +0100167 })) as Message;
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500168 let component: MessageComponentInteraction;
pineafanad54d752022-04-18 19:01:43 +0100169 try {
PineaFan0d06edc2023-01-17 22:10:31 +0000170 component = await m.awaitMessageComponent({
171 time: 300000,
TheCodedProf267563a2023-01-21 17:00:57 -0500172 filter: (i) => { return i.user.id === interaction.user.id && i.channel!.id === interaction.channel!.id && i.message.id === m.id }
PineaFan0d06edc2023-01-17 22:10:31 +0000173 });
pineafanad54d752022-04-18 19:01:43 +0100174 } catch (e) {
Skyler Greyad002172022-08-16 18:48:26 +0100175 timedOut = true;
176 continue;
pineafanad54d752022-04-18 19:01:43 +0100177 }
pineafan63fc5e22022-08-04 22:04:10 +0100178 component.deferUpdate();
pineafane23c4ec2022-07-27 21:56:27 +0100179 if (component.customId === "conflict") {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500180 const rolesToRemove = selected.filter((role) => role !== (component as StringSelectMenuInteraction).values[0]);
pineafan63fc5e22022-08-04 22:04:10 +0100181 await member.roles.remove(rolesToRemove);
pineafane23c4ec2022-07-27 21:56:27 +0100182 } else if (component.customId === "promote") {
pineafanad54d752022-04-18 19:01:43 +0100183 if (
Skyler Grey75ea9172022-08-06 10:22:23 +0100184 currentRoleIndex === -1
185 ? allowed[data.track.length - 1]
186 : allowed[currentRoleIndex - 1] && allowed[currentRoleIndex]
pineafanad54d752022-04-18 19:01:43 +0100187 ) {
pineafane23c4ec2022-07-27 21:56:27 +0100188 if (currentRoleIndex === -1) {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500189 await member.roles.add(data.track[data.track.length - 1]!);
pineafanad54d752022-04-18 19:01:43 +0100190 } else if (currentRoleIndex < data.track.length) {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500191 if (!data.retainPrevious) await member.roles.remove(data.track[currentRoleIndex]!);
192 await member.roles.add(data.track[currentRoleIndex - 1]!);
pineafanad54d752022-04-18 19:01:43 +0100193 }
194 }
pineafane23c4ec2022-07-27 21:56:27 +0100195 } else if (component.customId === "demote") {
Skyler Grey75ea9172022-08-06 10:22:23 +0100196 if (allowed[currentRoleIndex]) {
pineafane23c4ec2022-07-27 21:56:27 +0100197 if (currentRoleIndex === data.track.length - 1) {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500198 if (data.nullable) await member.roles.remove(data.track[currentRoleIndex]!);
pineafanad54d752022-04-18 19:01:43 +0100199 } else if (currentRoleIndex > -1) {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500200 await member.roles.remove(data.track[currentRoleIndex]!);
201 await member.roles.add(data.track[currentRoleIndex + 1]!);
pineafanad54d752022-04-18 19:01:43 +0100202 }
203 }
pineafane23c4ec2022-07-27 21:56:27 +0100204 } else if (component.customId === "select") {
PineaFan638eb132023-01-19 10:41:22 +0000205 track = parseInt((component as StringSelectMenuInteraction).values[0]!);
pineafanad54d752022-04-18 19:01:43 +0100206 }
207 }
pineafan63fc5e22022-08-04 22:04:10 +0100208};
pineafanad54d752022-04-18 19:01:43 +0100209
TheCodedProff86ba092023-01-27 17:10:07 -0500210const check = async (interaction: CommandInteraction, _partial: boolean = false) => {
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500211 const tracks = (await client.database.guilds.read(interaction.guild!.id)).tracks;
PineaFan5d98a4b2023-01-19 16:15:47 +0000212 if (tracks.length === 0) return "This server does not have any tracks";
Skyler Grey75ea9172022-08-06 10:22:23 +0100213 const member = interaction.member as GuildMember;
pineafanad54d752022-04-18 19:01:43 +0100214 // Allow the owner to promote anyone
TheCodedProf2ec2ba62023-01-18 22:26:20 -0500215 if (member.id === interaction.guild!.ownerId) return true;
pineafan6702cef2022-06-13 17:52:37 +0100216 // Check if the user can manage any of the tracks
pineafan63fc5e22022-08-04 22:04:10 +0100217 let managed = false;
PineaFan638eb132023-01-19 10:41:22 +0000218 const memberRoles = member.roles.cache.map((r) => r.id)
pineafanc1c18792022-08-03 21:41:36 +0100219 for (const element of tracks) {
PineaFan638eb132023-01-19 10:41:22 +0000220 if (!element.manageableBy.length) continue;
221 if (!element.manageableBy.some((role: string) => memberRoles.includes(role))) continue;
pineafanc1c18792022-08-03 21:41:36 +0100222 managed = true;
223 break;
pineafan63fc5e22022-08-04 22:04:10 +0100224 }
pineafanad54d752022-04-18 19:01:43 +0100225 // Check if the user has manage_roles permission
PineaFan5d98a4b2023-01-19 16:15:47 +0000226 if (!managed && !member.permissions.has("ManageRoles")) return "You do not have the *Manage Roles* permission";
pineafanad54d752022-04-18 19:01:43 +0100227 // Allow track
pineafan6702cef2022-06-13 17:52:37 +0100228 return true;
pineafan63fc5e22022-08-04 22:04:10 +0100229};
pineafanad54d752022-04-18 19:01:43 +0100230
231export { command };
232export { callback };
pineafanc6158ab2022-06-17 16:34:07 +0100233export { check };