Skip to content

/modmail slash command #329

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
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 @@ -70,6 +71,10 @@ public enum Commands {
commands.add(new MuteCommand(actionsStore));
commands.add(new UnmuteCommand(actionsStore));

ModmailCommand modmailCommand = new ModmailCommand();
commands.add(modmailCommand);
commands.add(modmailCommand.new ReloadModmailCommand());
Copy link
Contributor

Choose a reason for hiding this comment

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

This will not compile.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Really? I'm sure I've tested and ran it on my machine numerous times.

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 also ran gradle build so I'm really sure it works. Do you mind checking it again?

Copy link
Member

Choose a reason for hiding this comment

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

It doesn't compile no, I'm not sure what you're doing there modmailCommand.new ReloadModmailCommand()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ReloadModmailCommand is an inner class since it's the only solution I could think of to reload the mods in the /modmail command. Thus, I am instantiating the ReloadModmailCommand from the Modmail object.

Copy link
Member

Choose a reason for hiding this comment

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

Ah that explains

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 am genuinely confused why it doesn't compile. Do you mind helping me out on this one?

Copy link
Member

@Tais993 Tais993 Dec 18, 2021

Choose a reason for hiding this comment

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

Well, it compiles like you said, it's just done in a weird way.
To my knowledge, you can remove modmailCommand. from modmailCommand.new ReloadModmailCommand()
So please do that, if you can't, refer to the class instead of an instance?

Copy link
Contributor

Choose a reason for hiding this comment

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

You can't. That's the required syntax for instantiating a non-static inner class. It's really weird.


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

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.ReadyEvent;
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.ArrayList;
import java.util.List;
import java.util.Objects;

/**
* Command that either sends a direct message to a moderator or sends the message to the dedicated
* mod audit channel by the moderators.
*/
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();

final List<SelectOption> mods = new ArrayList<>();

/**
* Creates an instance of the ModMail command.
*/
public ModmailCommand() {
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);

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

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

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.
String userId = args.get(0);
if (!userId.equals(event.getUser().getId())) {
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;
}

// disable selection menu
event.getMessage().editMessageComponents(ActionRow.of(disabledMenu)).queue();

boolean wasSent = sendToMod(modId, message, event);
if (!wasSent) {
event.reply("The moderator you chose was not found on the guild.")
.setEphemeral(true)
.queue();

String modSelectedByUser = event.getSelectedOptions().get(0).getLabel();
logger.warn("""
Moderator '{}' chosen by user is not on the guild. Use the /reloadmod command
to update the list of moderators.
""", modSelectedByUser);

return;
}

event.reply("Message now sent to selected moderator").setEphemeral(true).queue();
}

/**
* Populates the list of moderators and stores it into a list to avoid querying an expensive
* call to discord everytime the command is used.
*
* @param event the event that triggered this method
*/
@Override
public void onReady(@NotNull ReadyEvent event) {
Guild guild = event.getJDA().getGuildById(config.getGuildId());
ModmailUtil.listOfMod(guild, mods);
}

private boolean sendToMod(@NotNull String modId, @NotNull String message,
@NotNull SelectionMenuEvent event) {
// the else is when the user invoked the command not on the context of a guild.
Guild guild = Objects.requireNonNullElse(event.getGuild(),
event.getJDA().getGuildById(config.getGuildId()));

return !guild.retrieveMemberById(modId)
.submit()
.thenCompose(user -> user.getUser().openPrivateChannel().submit())
.thenAccept(channel -> channel
.sendMessageEmbeds(ModmailUtil.messageEmbed(event.getUser().getName(), message))
.queue())
.whenComplete((v, err) -> {
if (err != null)
err.printStackTrace();
})
.isCompletedExceptionally();
}

/**
* Reloads the list of moderators to choose from from the {@link ModmailCommand}.
* <p>
* Only members who have the Moderator role can use this command.
*/
public class ReloadModmailCommand extends SlashCommandAdapter {

private static final String COMMAND_NAME = "reloadmod";

public ReloadModmailCommand() {
super(COMMAND_NAME, "reloads the moderators in the modmail command",
SlashCommandVisibility.GUILD);
}

@Override
public void onSlashCommand(@NotNull SlashCommandEvent event) {
mods.clear();
ModmailUtil.listOfMod(event.getGuild(), mods);

if (ModmailUtil.doesUserHaveModRole(event.getMember(), event.getGuild())) {
event.reply("List of moderators has now been updated.").setEphemeral(true).queue();
return;
}

event.reply("Only moderators can use this command.").setEphemeral(true).queue();
}

}


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

import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.MessageEmbed;
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.awt.*;
import java.util.List;
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();
}

/**
* Finds the moderators on the given guild and stores it for later use.
* <p>
* This call is expensive, thus, it should only be used preferably once by storing the result
* from the given list or seldomly when moderators want to reload the list of moderators to
* choose from from the {@link ModmailCommand}.
* <p/>
* Since the elements in the given list will not be overwritten, the caller is responsible in
* doing such actions.
*/
public static void listOfMod(@NotNull Guild guild, List<SelectOption> modsOptions) {
Role modRole = getModRole(guild)
.orElseThrow(() -> new IllegalStateException("No moderator role found"));

guild.findMembersWithRoles(modRole)
.onSuccess(mods -> mods.forEach(
mod -> modsOptions.add(SelectOption.of(mod.getEffectiveName(), mod.getId()))));
}

/**
* 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.
* <p>
* See {@link Config#getHeavyModerationRolePattern()}.
*
* @param member the member to check for moderator role.
* @param guild the guild to get the moderator role from.
* @return true if the member has the role Moderator
*/
public static boolean doesUserHaveModRole(@NotNull Member member, @NotNull Guild guild) {
return member.canInteract(getModRole(guild)
.orElseThrow(() -> new IllegalStateException("No moderator role found")));
}

/**
* Creates a color black {@link MessageEmbed} with a non-inline field of the supplied message.
*
* @param user the user who invoked the command.
* @param message the message the user wants to send to to a moderator or the moderators.
* @return returns a {@link MessageEmbed} to send to the moderator.
*/
public static MessageEmbed messageEmbed(@NotNull String user, @NotNull String message) {
return new EmbedBuilder().setAuthor("Modmail Command invoked")
.setColor(Color.BLACK)
.setTitle("Message from user '%s' who used /modmail command".formatted(user))
.addField("Message", message, false)
.build();
}

}
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;
}
}