Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2cb5974
Draft commit for /modmail slash command.
talentedasian Dec 15, 2021
52744e5
Instead of getting the mods with an expensive query, query it just once
talentedasian Dec 16, 2021
806bee3
Send DM to selected mod. If the selected mod wasn't found, which is
talentedasian Dec 16, 2021
b563a4c
Reply an ephemeral message to user after sending the message to a mod…
talentedasian Dec 16, 2021
7ec6b33
Miscellaneous refactors
talentedasian Dec 16, 2021
1bfbf42
* Add /reloadmod command for moderators to reload the list of choices in
talentedasian Dec 16, 2021
e154d7f
* Completely redo the way how select options are being generated.
talentedasian Dec 17, 2021
4e471f4
Draft commit for /modmail slash command.
talentedasian Dec 15, 2021
df96610
Instead of getting the mods with an expensive query, query it just once
talentedasian Dec 16, 2021
a923902
Send DM to selected mod. If the selected mod wasn't found, which is
talentedasian Dec 16, 2021
67a63d8
Reply an ephemeral message to user after sending the message to a mod…
talentedasian Dec 16, 2021
d4a99a3
Miscellaneous refactors
talentedasian Dec 16, 2021
41cb0dd
* Add /reloadmod command for moderators to reload the list of choices in
talentedasian Dec 16, 2021
522b3fd
* Completely redo the way how select options are being generated.
talentedasian Dec 17, 2021
ad9c2ec
Merge remote-tracking branch 'talentedasian/feature/modmail' into fea…
Tais993 Jan 20, 2022
c1b34f6
SplotlessApply - apply + cry = accurate
Tais993 Jan 27, 2022
6703c11
Merge branch 'develop' of https://github.com/Together-Java/TJ-Bot int…
Tais993 Jan 27, 2022
a65831e
Added ModmailCommand to the Features
Tais993 Jan 27, 2022
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 @@ -8,6 +8,7 @@
import org.togetherjava.tjbot.commands.mathcommands.TeXCommand;
import org.togetherjava.tjbot.commands.moderation.*;
import org.togetherjava.tjbot.commands.moderation.temp.TemporaryModerationRoutine;
import org.togetherjava.tjbot.commands.modmail.ModmailCommand;
import org.togetherjava.tjbot.commands.system.BotCore;
import org.togetherjava.tjbot.commands.tags.TagCommand;
import org.togetherjava.tjbot.commands.tags.TagManageCommand;
Expand Down Expand Up @@ -59,7 +60,8 @@ public enum Features {
// Message receivers

// Event receivers
features.add(new RejoinMuteListener(actionsStore));
features.add(new UnmuteCommand(actionsStore));
features.add(new ModmailCommand());

// Slash commands
features.add(new PingCommand());
Expand All @@ -81,4 +83,4 @@ public enum Features {

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