Skip to content

Commit 49303cc

Browse files
Tais993JJeeff248
andauthored
Reaction roles using a SelectionMenu (#351)
Reaction roles Added RoleSelectCommand This command allows people with the `MANAGE_ROLES` permission to create a menu. This menu can be used by users to add roles to themselves. Co-authored-by: JJeeff248 <[email protected]>
1 parent 4f39125 commit 49303cc

File tree

2 files changed

+330
-0
lines changed

2 files changed

+330
-0
lines changed

application/src/main/java/org/togetherjava/tjbot/commands/Features.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.dv8tion.jda.api.JDA;
44
import org.jetbrains.annotations.NotNull;
55
import org.togetherjava.tjbot.commands.basic.PingCommand;
6+
import org.togetherjava.tjbot.commands.basic.RoleSelectCommand;
67
import org.togetherjava.tjbot.commands.basic.VcActivityCommand;
78
import org.togetherjava.tjbot.commands.free.FreeCommand;
89
import org.togetherjava.tjbot.commands.mathcommands.TeXCommand;
@@ -78,6 +79,7 @@ public enum Features {
7879
features.add(new AuditCommand(actionsStore));
7980
features.add(new MuteCommand(actionsStore));
8081
features.add(new UnmuteCommand(actionsStore));
82+
features.add(new RoleSelectCommand());
8183
features.add(new TopHelpersCommand(database));
8284

8385
// Mixtures
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
package org.togetherjava.tjbot.commands.basic;
2+
3+
import net.dv8tion.jda.api.EmbedBuilder;
4+
import net.dv8tion.jda.api.MessageBuilder;
5+
import net.dv8tion.jda.api.Permission;
6+
import net.dv8tion.jda.api.entities.*;
7+
import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent;
8+
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
9+
import net.dv8tion.jda.api.interactions.Interaction;
10+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
11+
import net.dv8tion.jda.api.interactions.commands.OptionType;
12+
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
13+
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
14+
import net.dv8tion.jda.api.interactions.components.ActionRow;
15+
import net.dv8tion.jda.api.interactions.components.ComponentInteraction;
16+
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
17+
import net.dv8tion.jda.api.interactions.components.selections.SelectionMenu;
18+
import org.jetbrains.annotations.Contract;
19+
import org.jetbrains.annotations.NotNull;
20+
import org.jetbrains.annotations.Nullable;
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
24+
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
25+
import org.togetherjava.tjbot.commands.componentids.Lifespan;
26+
27+
import java.awt.*;
28+
import java.util.ArrayList;
29+
import java.util.Collection;
30+
import java.util.List;
31+
import java.util.Objects;
32+
import java.util.function.Function;
33+
34+
35+
/**
36+
* Implements the {@code roleSelect} command.
37+
*
38+
* <p>
39+
* Allows users to select their roles without using reactions, instead it uses selection menus where
40+
* you can select multiple roles. <br />
41+
* Note: the bot can only use roles with a position below its highest one
42+
*/
43+
public final class RoleSelectCommand extends SlashCommandAdapter {
44+
45+
private static final Logger logger = LoggerFactory.getLogger(RoleSelectCommand.class);
46+
47+
private static final String ALL_OPTION = "all";
48+
private static final String CHOOSE_OPTION = "choose";
49+
50+
private static final String TITLE_OPTION = "title";
51+
private static final String DESCRIPTION_OPTION = "description";
52+
53+
private static final Color AMBIENT_COLOR = new Color(24, 221, 136, 255);
54+
55+
private static final List<OptionData> messageOptions = List.of(
56+
new OptionData(OptionType.STRING, TITLE_OPTION, "The title for the message", false),
57+
new OptionData(OptionType.STRING, DESCRIPTION_OPTION, "A description for the message",
58+
false));
59+
60+
61+
/**
62+
* Construct an instance.
63+
*/
64+
public RoleSelectCommand() {
65+
super("role-select", "Sends a message where users can select their roles",
66+
SlashCommandVisibility.GUILD);
67+
68+
SubcommandData allRoles =
69+
new SubcommandData(ALL_OPTION, "Lists all the rolls in the server for users")
70+
.addOptions(messageOptions);
71+
72+
SubcommandData selectRoles =
73+
new SubcommandData(CHOOSE_OPTION, "Choose the roles for users to select")
74+
.addOptions(messageOptions);
75+
76+
getData().addSubcommands(allRoles, selectRoles);
77+
}
78+
79+
@NotNull
80+
private static SelectOption mapToSelectOption(@NotNull Role role) {
81+
RoleIcon roleIcon = role.getIcon();
82+
83+
if (null == roleIcon || !roleIcon.isEmoji()) {
84+
return SelectOption.of(role.getName(), role.getId());
85+
} else {
86+
return SelectOption.of(role.getName(), role.getId())
87+
.withEmoji((Emoji.fromUnicode(roleIcon.getEmoji())));
88+
}
89+
}
90+
91+
@Override
92+
public void onSlashCommand(@NotNull final SlashCommandEvent event) {
93+
Member member = Objects.requireNonNull(event.getMember(), "Member is null");
94+
if (!member.hasPermission(Permission.MANAGE_ROLES)) {
95+
event.reply("You dont have the right permissions to use this command")
96+
.setEphemeral(true)
97+
.queue();
98+
return;
99+
}
100+
101+
Member selfMember = Objects.requireNonNull(event.getGuild()).getSelfMember();
102+
if (!selfMember.hasPermission(Permission.MANAGE_ROLES)) {
103+
event.reply("The bot needs the manage role permissions").setEphemeral(true).queue();
104+
logger.error("The bot needs the manage role permissions");
105+
return;
106+
}
107+
108+
SelectionMenu.Builder menu =
109+
SelectionMenu.create(generateComponentId(Lifespan.PERMANENT, member.getId()));
110+
boolean isEphemeral = false;
111+
112+
if (CHOOSE_OPTION.equals(event.getSubcommandName())) {
113+
addMenuOptions(event, menu, "Select the roles to display", 1);
114+
isEphemeral = true;
115+
} else {
116+
addMenuOptions(event, menu, "Select your roles", 0);
117+
}
118+
119+
// Handle Optional arguments
120+
OptionMapping titleOption = event.getOption(TITLE_OPTION);
121+
OptionMapping descriptionOption = event.getOption(DESCRIPTION_OPTION);
122+
123+
String title = handleOption(titleOption);
124+
String description = handleOption(descriptionOption);
125+
126+
MessageBuilder messageBuilder = new MessageBuilder(makeEmbed(title, description))
127+
.setActionRows(ActionRow.of(menu.build()));
128+
129+
if (isEphemeral) {
130+
event.reply(messageBuilder.build()).setEphemeral(true).queue();
131+
} else {
132+
event.getChannel().sendMessage(messageBuilder.build()).queue();
133+
134+
event.reply("Message sent successfully!").setEphemeral(true).queue();
135+
}
136+
}
137+
138+
/**
139+
* Adds role options to a selection menu.
140+
* <p>
141+
*
142+
* @param event the {@link SlashCommandEvent}
143+
* @param menu the menu to add options to {@link SelectionMenu.Builder}
144+
* @param placeHolder the placeholder for the menu {@link String}
145+
* @param minValues the minimum number of selections. nullable {@link Integer}
146+
*/
147+
private static void addMenuOptions(@NotNull final Interaction event,
148+
@NotNull final SelectionMenu.Builder menu, @NotNull final String placeHolder,
149+
@Nullable final Integer minValues) {
150+
151+
Guild guild = Objects.requireNonNull(event.getGuild(), "The given guild cannot be null");
152+
153+
Role highestBotRole = guild.getSelfMember().getRoles().get(0);
154+
List<Role> guildRoles = guild.getRoles();
155+
156+
Collection<Role> roles = new ArrayList<>(
157+
guildRoles.subList(guildRoles.indexOf(highestBotRole) + 1, guildRoles.size()));
158+
159+
if (null != minValues) {
160+
menu.setMinValues(minValues);
161+
}
162+
163+
menu.setPlaceholder(placeHolder)
164+
.setMaxValues(roles.size())
165+
.addOptions(roles.stream()
166+
.filter(role -> !role.isPublicRole())
167+
.filter(role -> !role.getTags().isBot())
168+
.map(RoleSelectCommand::mapToSelectOption)
169+
.toList());
170+
}
171+
172+
/**
173+
* Creates an embedded message to send with the selection menu.
174+
*
175+
* @param title for the embedded message. nullable {@link String}
176+
* @param description for the embedded message. nullable {@link String}
177+
* @return the formatted embed {@link MessageEmbed}
178+
*/
179+
private static @NotNull MessageEmbed makeEmbed(@Nullable final String title,
180+
@Nullable final CharSequence description) {
181+
182+
String effectiveTitle = (null == title) ? "Select your roles:" : title;
183+
184+
return new EmbedBuilder().setTitle(effectiveTitle)
185+
.setDescription(description)
186+
.setColor(AMBIENT_COLOR)
187+
.build();
188+
}
189+
190+
@Override
191+
public void onSelectionMenu(@NotNull final SelectionMenuEvent event,
192+
@NotNull final List<String> args) {
193+
194+
Guild guild = Objects.requireNonNull(event.getGuild(), "The given guild cannot be null");
195+
List<SelectOption> selectedOptions = Objects.requireNonNull(event.getSelectedOptions(),
196+
"The given selectedOptions cannot be null");
197+
198+
List<Role> selectedRoles = selectedOptions.stream()
199+
.map(SelectOption::getValue)
200+
.map(guild::getRoleById)
201+
.filter(Objects::nonNull)
202+
.filter(role -> guild.getSelfMember().canInteract(role))
203+
.toList();
204+
205+
206+
if (event.getMessage().isEphemeral()) {
207+
handleNewRoleBuilderSelection(event, selectedRoles);
208+
} else {
209+
handleRoleSelection(event, selectedRoles, guild);
210+
}
211+
}
212+
213+
/**
214+
* Handles selection of a {@link SelectionMenuEvent}.
215+
*
216+
* @param event the <b>unacknowledged</b> {@link SelectionMenuEvent}
217+
* @param selectedRoles the {@link Role roles} selected
218+
* @param guild the {@link Guild}
219+
*/
220+
private static void handleRoleSelection(final @NotNull SelectionMenuEvent event,
221+
final @NotNull Collection<Role> selectedRoles, final Guild guild) {
222+
Collection<Role> rolesToAdd = new ArrayList<>(selectedRoles.size());
223+
Collection<Role> rolesToRemove = new ArrayList<>(selectedRoles.size());
224+
225+
event.getInteraction()
226+
.getComponent()
227+
.getOptions()
228+
.stream()
229+
.map(roleFromSelectOptionFunction(guild))
230+
.filter(Objects::nonNull)
231+
.forEach(role -> {
232+
if (selectedRoles.contains(role)) {
233+
rolesToAdd.add(role);
234+
} else {
235+
rolesToRemove.add(role);
236+
}
237+
});
238+
239+
handleRoleModifications(event, event.getMember(), guild, rolesToAdd, rolesToRemove);
240+
}
241+
242+
@NotNull
243+
private static Function<SelectOption, Role> roleFromSelectOptionFunction(Guild guild) {
244+
return selectedOption -> {
245+
Role role = guild.getRoleById(selectedOption.getValue());
246+
247+
if (null == role) {
248+
handleNullRole(selectedOption);
249+
}
250+
251+
return role;
252+
};
253+
}
254+
255+
/**
256+
* Handles the selection of the {@link SelectionMenu} if it came from a builder.
257+
*
258+
* @param event the <b>unacknowledged</b> {@link ComponentInteraction}
259+
* @param selectedRoles the {@link Role roles} selected by the {@link User} from the
260+
* {@link ComponentInteraction} event
261+
*/
262+
private void handleNewRoleBuilderSelection(@NotNull final ComponentInteraction event,
263+
final @NotNull Collection<? extends Role> selectedRoles) {
264+
SelectionMenu.Builder menu =
265+
SelectionMenu.create(generateComponentId(event.getUser().getId()))
266+
.setPlaceholder("Select your roles")
267+
.setMaxValues(selectedRoles.size())
268+
.setMinValues(0);
269+
270+
selectedRoles.forEach(role -> menu.addOption(role.getName(), role.getId()));
271+
272+
event.getChannel()
273+
.sendMessageEmbeds(event.getMessage().getEmbeds().get(0))
274+
.setActionRow(menu.build())
275+
.queue();
276+
277+
event.reply("Message sent successfully!").setEphemeral(true).queue();
278+
}
279+
280+
/**
281+
* Logs that the role of the given {@link SelectOption} doesn't exist anymore.
282+
*
283+
* @param selectedOption the {@link SelectOption}
284+
*/
285+
private static void handleNullRole(final @NotNull SelectOption selectedOption) {
286+
logger.info(
287+
"The {} ({}) role has been removed but is still an option in the selection menu",
288+
selectedOption.getLabel(), selectedOption.getValue());
289+
}
290+
291+
/**
292+
* Updates the roles of the given member.
293+
*
294+
* @param event an <b>unacknowledged</b> {@link Interaction} event
295+
* @param member the member to update the roles of
296+
* @param guild what guild to update the roles in
297+
* @param additionRoles the roles to add
298+
* @param removalRoles the roles to remove
299+
*/
300+
private static void handleRoleModifications(@NotNull final Interaction event,
301+
final Member member, final @NotNull Guild guild, final Collection<Role> additionRoles,
302+
final Collection<Role> removalRoles) {
303+
guild.modifyMemberRoles(member, additionRoles, removalRoles)
304+
.flatMap(empty -> event.reply("Your roles have been updated!").setEphemeral(true))
305+
.queue();
306+
}
307+
308+
/**
309+
* This gets the OptionMapping and returns the value as a string if there is one.
310+
*
311+
* @param option the {@link OptionMapping}
312+
* @return the value. nullable {@link String}
313+
*/
314+
@Contract("null -> null")
315+
private static @Nullable String handleOption(@Nullable final OptionMapping option) {
316+
if (null == option) {
317+
return null;
318+
}
319+
320+
if (OptionType.STRING == option.getType()) {
321+
return option.getAsString();
322+
} else if (OptionType.BOOLEAN == option.getType()) {
323+
return option.getAsBoolean() ? "true" : "false";
324+
} else {
325+
return null;
326+
}
327+
}
328+
}

0 commit comments

Comments
 (0)