Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.togetherjava.tjbot.commands.free.FreeCommand;
import org.togetherjava.tjbot.commands.mathcommands.TeXCommand;
import org.togetherjava.tjbot.commands.moderation.*;
import org.togetherjava.tjbot.commands.modmail.ModmailCommand;
import org.togetherjava.tjbot.commands.tags.TagCommand;
import org.togetherjava.tjbot.commands.tags.TagManageCommand;
import org.togetherjava.tjbot.commands.tags.TagSystem;
Expand Down Expand Up @@ -69,6 +70,8 @@ public enum Commands {
commands.add(new AuditCommand(actionsStore));
commands.add(new MuteCommand(actionsStore));
commands.add(new UnmuteCommand(actionsStore));
commands.add(new ModmailCommand(jda));
commands.add(new ReloadModMailCommand(jda));

return commands;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.togetherjava.tjbot.commands.moderation;

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import org.jetbrains.annotations.NotNull;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.commands.modmail.ModMailUtil;
import org.togetherjava.tjbot.config.Config;

import java.util.Objects;

public class ReloadModMailCommand extends SlashCommandAdapter {

private final JDA jda;
private static final Config config = Config.getInstance();

/**
* Creates an instance of ReloadMod command.
*
* @param jda the JDA instance to find the guild.
*/
public ReloadModMailCommand(@NotNull JDA jda) {
super("reloadmod", "Reloads the list of moderators in the modmail selection menu",
SlashCommandVisibility.GUILD);

this.jda = Objects.requireNonNull(jda);
}

@Override
public void onSlashCommand(@NotNull SlashCommandEvent event) {
Guild guild = Objects.requireNonNull(jda.getGuildById(config.getGuildId()),
"A Guild is required to use this command. Perhaps the bot isn't on the guild yet");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should be able to do event.getGuild(), no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could but I didn't due to some pedantic reason. If the command was called on Guild A, but the guildId was Guild B, then it would have inconsistent results.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about that, I think I could perhaps do event.getGuild() on the ModMailCommand as well.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think you should SlashCommandEvent#getGuild ^^

If you run the command in Guild B, right now it runs it like it's in Guild A, which is really weird imo

The bot is only in 1 guild, and it'll never join another one


if (ModMailUtil.doesUserHaveModRole(event.getMember(), guild)) {
event.reply("Only moderators can use this command.").setEphemeral(true).queue();
return;
}

ModMailUtil.loadMenuOptions(guild);
event.reply("List of moderators has now been reloaded.").setEphemeral(true).queue();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.togetherjava.tjbot.commands.modmail;

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
import org.jetbrains.annotations.NotNull;
import org.togetherjava.tjbot.config.Config;

import java.util.Optional;
import java.util.function.Predicate;
import java.util.regex.Pattern;

/**
* Utility methods that interact either directly or indirectly with the {@link ModmailCommand}.
*/
public class ModMailUtil {

public static final Predicate<String> isModRole =
Pattern.compile(Config.getInstance().getHeavyModerationRolePattern())
.asMatchPredicate();

private ModMailUtil() {
throw new UnsupportedOperationException();
}

/**
* Clears the list of moderators in the {@link ModmailCommand}.
*/
public static void clearMods() {
ModmailCommand.mods.clear();
ModmailCommand.modsMap.clear();
}

/**
* Finds the moderators on the given guild and stores it for later use.
* <p>
* This call is expensive to make thus, it shall only be used preferably once by storing the
* result somewhere or seldomly when moderators want to reload the list of moderators to choose
* from from the {@link ModmailCommand}.
* <p/>
* The previous result stored by this method will be overwritten if there was any.
*/
public static void loadMenuOptions(@NotNull Guild guild) {
clearMods();

Role modRole = getModRole(guild)
.orElseThrow(() -> new IllegalStateException("No moderator role found"));

guild.findMembersWithRoles(modRole).get().stream().forEach(mod -> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the get and the stream?

Just use onSuccess?

String modId = mod.getId();
ModmailCommand.mods.add(SelectOption.of(mod.getEffectiveName(), modId));
ModmailCommand.modsMap.put(modId, mod.getUser());
});
}

/**
* Gets the moderator role.
*
* @param guild the guild to get the moderator role from
* @return the moderator role, if found
*/
public static @NotNull Optional<Role> getModRole(@NotNull Guild guild) {
return guild.getRoles().stream().filter(role -> isModRole.test(role.getName())).findAny();
}

/**
* Checks whether the given member is a moderator on the given guild.
*
* @param member the member to check for moderator role.
* @param guild the guild to get the moderator role from.
* @return
*/
public static boolean doesUserHaveModRole(@NotNull Member member, @NotNull Guild guild) {
return member.canInteract(getModRole(guild)
.orElseThrow(() -> new IllegalStateException("No moderator role found")));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package org.togetherjava.tjbot.commands.modmail;

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.SelectionMenuEvent;
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
import net.dv8tion.jda.api.interactions.components.selections.SelectionMenu;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.config.Config;

import java.util.*;

public class ModmailCommand extends SlashCommandAdapter {
private static final Logger logger = LoggerFactory.getLogger(ModmailCommand.class);

private static final String COMMAND_NAME = "modmail";
private static final String TARGET_OPTION = "message";
private static final Config config = Config.getInstance();

static final List<SelectOption> mods = new ArrayList<>();
static final Map<String, User> modsMap = new HashMap<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be done this way, keep the variables private, use the onReady event to load the options/mods.

So keep it within this class, make the variables private and don't make them static.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another note, NEVER store Discord entities.

They can be invalidated (garbage collected) which will result in really weird errors.

Instead, store their ID (or username in this case)


private final JDA jda;

/**
* Creates an instance of the ModMail command.
*
* @param jda the {@link JDA} instance of all slash commands to find the target guild or the
* guild where the moderators are.
*/
public ModmailCommand(@NotNull JDA jda) {
super(COMMAND_NAME,
"sends a message to either a single moderator or on the mod_audit_log channel",
SlashCommandVisibility.GLOBAL);

getData().addOption(OptionType.STRING, TARGET_OPTION, "The message to send", true);

this.jda = Objects.requireNonNull(jda);

mods.add(SelectOption.of("All Moderators", "all"));
}

@Override
public void onSlashCommand(@NotNull SlashCommandEvent event) {
String memberId = event.getUser().getId();

// checks if selection menu already contains the moderators
if (mods.size() == 1) {
loadMenuOptions();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this in favor of the onReady event, see above


event.reply("""
Select the moderator to send message to, or select "All Moderators" to send to
the guild's mod audit channel.
""")
.addActionRow(SelectionMenu
.create(generateComponentId(memberId, event.getOption(TARGET_OPTION).getAsString()))
.addOptions(mods)
.build())
.setEphemeral(false)
.queue();
}

@Override
public void onSelectionMenu(@NotNull SelectionMenuEvent event, @NotNull List<String> args) {
String message = args.get(1);
// Ignore if another user clicked the button which is only possible when used within the
// guild.
String userId = args.get(0);
if (event.isFromGuild()
&& !userId.equals(Objects.requireNonNull(event.getMember()).getId())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&& !userId.equals(Objects.requireNonNull(event.getMember()).getId())) {
&& !userId.equals(event.getMember().getId())) {

You're effectively doing

if (member == null) {
    member.toString();
}

Here, which is, useless

event.reply(
"Sorry, but only the user who triggered the command can interact with the menu.")
.setEphemeral(true)
.queue();
return;
}

SelectionMenu disabledMenu = event.getSelectionMenu().asDisabled();

// did user select to send message to all mods
String modId = event.getValues().get(0);
if (modId.equals("all")) {
// currently blocked by #296
event.reply("Message now sent to all mods").setEphemeral(true).queue();
return;
}

sendToMod(modId, message, event);

event.getMessage().editMessageComponents(ActionRow.of(disabledMenu)).queue();
event.reply("Message now sent to moderator").setEphemeral(true).queue();
}

private void sendToMod(@NotNull String modId, @NotNull String message,
@NotNull SelectionMenuEvent event) {
User mod = modsMap.get(modId);
if (mod != null) {
mod.openPrivateChannel().queue(channel -> channel.sendMessage(message).queue());
return;
}

logger
.warn("""
The map storing the moderators is either not in-sync with the list of moderators for the selection menu or
an unknown error has occurred.
""");

event.reply("The moderator you chose is not on the list of moderators on the guild")
.setEphemeral(true)
.queue();
}

/**
* Creates a list of options containing the moderators for use in the modmail slash command.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to change the javadoc for this. I'll just do it tomorrow.

* <p/>
* If this method has not yet been called prior to calling this method, it will call an
* expensive query to discord, otherwise, it will return the previous result.
* <p>
* This method also stores the moderators on a map for later use. The map's values are always
* and should be exactly the same with the previous results.
*/
private void loadMenuOptions() {
Guild guild = Objects.requireNonNull(jda.getGuildById(config.getGuildId()),
"A Guild is required to use this command. Perhaps the bot isn't on the guild yet");

ModMailUtil.loadMenuOptions(guild);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See review above, remove this in favor of the onReady event


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jetbrains.annotations.NotNull;
import org.togetherjava.tjbot.commands.modmail.ModmailCommand;

import java.io.IOException;
import java.nio.file.Path;
Expand Down Expand Up @@ -32,6 +33,7 @@ public final class Config {
private final String heavyModerationRolePattern;
private final String softModerationRolePattern;
private final String tagManageRolePattern;
private final String guildId;

private final List<FreeCommandConfig> freeCommand;

Expand All @@ -46,6 +48,7 @@ private Config(@JsonProperty("token") String token,
@JsonProperty("heavyModerationRolePattern") String heavyModerationRolePattern,
@JsonProperty("softModerationRolePattern") String softModerationRolePattern,
@JsonProperty("tagManageRolePattern") String tagManageRolePattern,
@JsonProperty("guildId") String guildId,
@JsonProperty("freeCommand") List<FreeCommandConfig> freeCommand) {
this.token = token;
this.databasePath = databasePath;
Expand All @@ -56,6 +59,7 @@ private Config(@JsonProperty("token") String token,
this.heavyModerationRolePattern = heavyModerationRolePattern;
this.softModerationRolePattern = softModerationRolePattern;
this.tagManageRolePattern = tagManageRolePattern;
this.guildId = guildId;
this.freeCommand = Collections.unmodifiableList(freeCommand);
}

Expand Down Expand Up @@ -178,4 +182,13 @@ public String getTagManageRolePattern() {
public @NotNull Collection<FreeCommandConfig> getFreeCommandConfig() {
return freeCommand; // already unmodifiable
}

/**
* Gets the id of the guild. See {@link ModmailCommand} for such uses
*
* @return the guildId.
*/
public String getGuildId() {
return guildId;
}
}