-
-
Notifications
You must be signed in to change notification settings - Fork 89
Migrate kick, ban and unban command #244
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
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
92eba7e
Added moderation commands (Ban, Kick, Unban)
RealYusufIsmail 54fafc6
Fixed code flaws
Zabuzard 9329ce5
Linter issues
Zabuzard 0060f60
Readability fix (CR yusuf)
Zabuzard 4db3cc6
Moved several handle methods into helper
Zabuzard 445d617
Fixed some typos
Zabuzard a1c12f7
Not banning already banned users
Zabuzard db3d023
Added "unable to send DM" notice
Zabuzard 2f1aba0
Fixed bug with unbanning non-banned users
Zabuzard 9c8016f
Fixed some typos
Zabuzard b720238
Using embed instead of plain messages
Zabuzard 427c224
using constants to separate magic strings
Zabuzard f7d565f
Readability cleanup on the "checks"
Zabuzard c2dd08d
NotNull for enum (CR by Tais)
Zabuzard 20a8fea
Making moderation commands role based instead of perms
Zabuzard 5fec89f
Added back permission based system, additionally as secondary layer
Zabuzard ecad42d
Spotless after rebase
Zabuzard 0460a19
empty line for readability (CR Tais)
Zabuzard c71ff66
Simplified ban/kick flow using individual methods (CR Tais)
Zabuzard 8b48e47
Moved failure handling for unban into method (CR Tais)
Zabuzard 60190a5
Missing imports after rebase
Zabuzard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
196 changes: 196 additions & 0 deletions
196
application/src/main/java/org/togetherjava/tjbot/commands/moderation/BanCommand.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
package org.togetherjava.tjbot.commands.moderation; | ||
|
||
import net.dv8tion.jda.api.Permission; | ||
import net.dv8tion.jda.api.entities.*; | ||
import net.dv8tion.jda.api.events.GenericEvent; | ||
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; | ||
import net.dv8tion.jda.api.exceptions.ErrorResponseException; | ||
import net.dv8tion.jda.api.interactions.Interaction; | ||
import net.dv8tion.jda.api.interactions.InteractionHook; | ||
import net.dv8tion.jda.api.interactions.commands.OptionMapping; | ||
import net.dv8tion.jda.api.interactions.commands.OptionType; | ||
import net.dv8tion.jda.api.interactions.commands.build.OptionData; | ||
import net.dv8tion.jda.api.requests.ErrorResponse; | ||
import net.dv8tion.jda.api.requests.RestAction; | ||
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; | ||
import net.dv8tion.jda.api.utils.Result; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
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.Objects; | ||
import java.util.Optional; | ||
import java.util.function.Predicate; | ||
import java.util.regex.Pattern; | ||
|
||
/** | ||
* This command can ban users and optionally remove their messages from the past days. Banning can | ||
* also be paired with a ban reason. The command will also try to DM the user to inform them about | ||
* the action and the reason. | ||
* <p> | ||
* The command fails if the user triggering it is lacking permissions to either ban other users or | ||
* to ban the specific given user (for example a moderator attempting to ban an admin). | ||
*/ | ||
public final class BanCommand extends SlashCommandAdapter { | ||
private static final Logger logger = LoggerFactory.getLogger(BanCommand.class); | ||
private static final String TARGET_OPTION = "user"; | ||
private static final String DELETE_HISTORY_OPTION = "delete-history"; | ||
private static final String REASON_OPTION = "reason"; | ||
private static final String COMMAND_NAME = "ban"; | ||
private static final String ACTION_VERB = "ban"; | ||
private final Predicate<String> hasRequiredRole; | ||
|
||
/** | ||
* Constructs an instance. | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public BanCommand() { | ||
super(COMMAND_NAME, "Bans the given user from the server", SlashCommandVisibility.GUILD); | ||
|
||
getData().addOption(OptionType.USER, TARGET_OPTION, "The user who you want to ban", true) | ||
.addOption(OptionType.STRING, REASON_OPTION, "Why the user should be banned", true) | ||
.addOptions(new OptionData(OptionType.INTEGER, DELETE_HISTORY_OPTION, | ||
"the amount of days of the message history to delete, none means no messages are deleted.", | ||
true).addChoice("none", 0).addChoice("recent", 1).addChoice("all", 7)); | ||
|
||
hasRequiredRole = Pattern.compile(Config.getInstance().getHeavyModerationRolePattern()) | ||
.asMatchPredicate(); | ||
} | ||
|
||
private static RestAction<InteractionHook> handleAlreadyBanned(@NotNull Guild.Ban ban, | ||
@NotNull Interaction event) { | ||
String reason = ban.getReason(); | ||
String reasonText = | ||
reason == null || reason.isBlank() ? "" : " (reason: %s)".formatted(reason); | ||
|
||
String message = "The user '%s' is already banned%s.".formatted(ban.getUser().getAsTag(), | ||
reasonText); | ||
return event.reply(message).setEphemeral(true); | ||
} | ||
|
||
@SuppressWarnings("MethodWithTooManyParameters") | ||
private static RestAction<InteractionHook> banUserFlow(@NotNull User target, | ||
@NotNull Member author, @NotNull String reason, int deleteHistoryDays, | ||
@NotNull Guild guild, @NotNull SlashCommandEvent event) { | ||
return sendDm(target, reason, guild, event) | ||
.flatMap(hasSentDm -> banUser(target, author, reason, deleteHistoryDays, guild) | ||
.map(banResult -> hasSentDm)) | ||
.map(hasSentDm -> sendFeedback(hasSentDm, target, author, reason)) | ||
.flatMap(event::replyEmbeds); | ||
} | ||
|
||
private static RestAction<Boolean> sendDm(@NotNull ISnowflake target, @NotNull String reason, | ||
@NotNull Guild guild, @NotNull GenericEvent event) { | ||
return event.getJDA() | ||
.openPrivateChannelById(target.getId()) | ||
.flatMap(channel -> channel.sendMessage( | ||
""" | ||
Hey there, sorry to tell you but unfortunately you have been banned from the server %s. | ||
If you think this was a mistake, please contact a moderator or admin of the server. | ||
The reason for the ban is: %s | ||
""" | ||
.formatted(guild.getName(), reason))) | ||
.mapToResult() | ||
.map(Result::isSuccess); | ||
} | ||
|
||
private static AuditableRestAction<Void> banUser(@NotNull User target, @NotNull Member author, | ||
@NotNull String reason, int deleteHistoryDays, @NotNull Guild guild) { | ||
logger.info( | ||
"'{}' ({}) banned the user '{}' ({}) from guild '{}' and deleted their message history of the last {} days, for reason '{}'.", | ||
author.getUser().getAsTag(), author.getId(), target.getAsTag(), target.getId(), | ||
guild.getName(), deleteHistoryDays, reason); | ||
|
||
return guild.ban(target, deleteHistoryDays, reason); | ||
} | ||
|
||
private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull User target, | ||
@NotNull Member author, @NotNull String reason) { | ||
String dmNoticeText = ""; | ||
if (!hasSentDm) { | ||
dmNoticeText = "(Unable to send them a DM.)"; | ||
} | ||
return ModerationUtils.createActionResponse(author.getUser(), ModerationUtils.Action.BAN, | ||
target, dmNoticeText, reason); | ||
} | ||
|
||
private static Optional<RestAction<InteractionHook>> handleNotAlreadyBannedResponse( | ||
@NotNull Throwable alreadyBannedFailure, @NotNull Interaction event, | ||
@NotNull Guild guild, @NotNull User target) { | ||
if (alreadyBannedFailure instanceof ErrorResponseException errorResponseException) { | ||
if (errorResponseException.getErrorResponse() == ErrorResponse.UNKNOWN_BAN) { | ||
return Optional.empty(); | ||
} | ||
|
||
if (errorResponseException.getErrorResponse() == ErrorResponse.MISSING_PERMISSIONS) { | ||
logger.error("The bot does not have the '{}' permission on the guild '{}'.", | ||
Permission.BAN_MEMBERS, guild.getName()); | ||
return Optional.of(event.reply( | ||
"I can not ban users in this guild since I do not have the %s permission." | ||
.formatted(Permission.BAN_MEMBERS)) | ||
.setEphemeral(true)); | ||
} | ||
} | ||
logger.warn("Something unexpected went wrong while trying to ban the user '{}'.", | ||
target.getAsTag(), alreadyBannedFailure); | ||
return Optional.of(event.reply("Failed to ban the user due to an unexpected problem.") | ||
.setEphemeral(true)); | ||
} | ||
|
||
@SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) | ||
private boolean handleChecks(@NotNull Member bot, @NotNull Member author, | ||
@Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, | ||
@NotNull Interaction event) { | ||
// Member doesn't exist if attempting to ban a user who is not part of the guild. | ||
if (target != null && !ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, | ||
target, event)) { | ||
return false; | ||
} | ||
if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { | ||
return false; | ||
} | ||
if (!ModerationUtils.handleHasBotPermissions(ACTION_VERB, Permission.BAN_MEMBERS, bot, | ||
guild, event)) { | ||
return false; | ||
} | ||
if (!ModerationUtils.handleHasAuthorPermissions(ACTION_VERB, Permission.BAN_MEMBERS, author, | ||
guild, event)) { | ||
return false; | ||
} | ||
return ModerationUtils.handleReason(reason, event); | ||
} | ||
|
||
@Override | ||
public void onSlashCommand(@NotNull SlashCommandEvent event) { | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
OptionMapping targetOption = | ||
Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null"); | ||
User target = targetOption.getAsUser(); | ||
Member author = Objects.requireNonNull(event.getMember(), "The author is null"); | ||
String reason = Objects.requireNonNull(event.getOption(REASON_OPTION), "The reason is null") | ||
.getAsString(); | ||
|
||
Guild guild = Objects.requireNonNull(event.getGuild()); | ||
Member bot = guild.getSelfMember(); | ||
|
||
if (!handleChecks(bot, author, targetOption.getAsMember(), reason, guild, event)) { | ||
return; | ||
} | ||
|
||
int deleteHistoryDays = Math | ||
.toIntExact(Objects.requireNonNull(event.getOption(DELETE_HISTORY_OPTION)).getAsLong()); | ||
|
||
// Ban the user, but only if not already banned | ||
guild.retrieveBan(target).mapToResult().flatMap(alreadyBanned -> { | ||
if (alreadyBanned.isSuccess()) { | ||
return handleAlreadyBanned(alreadyBanned.get(), event); | ||
} | ||
|
||
return handleNotAlreadyBannedResponse(Objects | ||
.requireNonNull(alreadyBanned.getFailure()), event, guild, target).orElseGet( | ||
() -> banUserFlow(target, author, reason, deleteHistoryDays, guild, event)); | ||
}).queue(); | ||
} | ||
} |
145 changes: 145 additions & 0 deletions
145
application/src/main/java/org/togetherjava/tjbot/commands/moderation/KickCommand.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package org.togetherjava.tjbot.commands.moderation; | ||
|
||
import net.dv8tion.jda.api.Permission; | ||
import net.dv8tion.jda.api.entities.*; | ||
import net.dv8tion.jda.api.events.GenericEvent; | ||
import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; | ||
import net.dv8tion.jda.api.interactions.Interaction; | ||
import net.dv8tion.jda.api.interactions.commands.OptionType; | ||
import net.dv8tion.jda.api.requests.RestAction; | ||
import net.dv8tion.jda.api.requests.restaction.AuditableRestAction; | ||
import net.dv8tion.jda.api.utils.Result; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.jetbrains.annotations.Nullable; | ||
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.Objects; | ||
import java.util.function.Predicate; | ||
import java.util.regex.Pattern; | ||
|
||
|
||
/** | ||
* This command can kicks users. Kicking can also be paired with a kick reason. The command will | ||
* also try to DM the user to inform them about the action and the reason. | ||
* <p> | ||
* The command fails if the user triggering it is lacking permissions to either kick other users or | ||
* to kick the specific given user (for example a moderator attempting to kick an admin). | ||
*/ | ||
public final class KickCommand extends SlashCommandAdapter { | ||
private static final Logger logger = LoggerFactory.getLogger(KickCommand.class); | ||
private static final String TARGET_OPTION = "user"; | ||
private static final String REASON_OPTION = "reason"; | ||
private static final String COMMAND_NAME = "kick"; | ||
private static final String ACTION_VERB = "kick"; | ||
private final Predicate<String> hasRequiredRole; | ||
|
||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/** | ||
* Constructs an instance. | ||
*/ | ||
Zabuzard marked this conversation as resolved.
Show resolved
Hide resolved
|
||
public KickCommand() { | ||
super(COMMAND_NAME, "Kicks the given user from the server", SlashCommandVisibility.GUILD); | ||
|
||
getData().addOption(OptionType.USER, TARGET_OPTION, "The user who you want to kick", true) | ||
.addOption(OptionType.STRING, REASON_OPTION, "Why the user should be kicked", true); | ||
|
||
hasRequiredRole = Pattern.compile(Config.getInstance().getSoftModerationRolePattern()) | ||
.asMatchPredicate(); | ||
} | ||
|
||
private static void handleAbsentTarget(@NotNull Interaction event) { | ||
event.reply("I can not kick the given user since they are not part of the guild anymore.") | ||
.setEphemeral(true) | ||
.queue(); | ||
} | ||
|
||
private static void kickUserFlow(@NotNull Member target, @NotNull Member author, | ||
@NotNull String reason, @NotNull Guild guild, @NotNull SlashCommandEvent event) { | ||
sendDm(target, reason, guild, event) | ||
.flatMap(hasSentDm -> kickUser(target, author, reason, guild) | ||
.map(kickResult -> hasSentDm)) | ||
.map(hasSentDm -> sendFeedback(hasSentDm, target, author, reason)) | ||
.flatMap(event::replyEmbeds) | ||
.queue(); | ||
} | ||
|
||
private static RestAction<Boolean> sendDm(@NotNull ISnowflake target, @NotNull String reason, | ||
@NotNull Guild guild, @NotNull GenericEvent event) { | ||
return event.getJDA() | ||
.openPrivateChannelById(target.getId()) | ||
.flatMap(channel -> channel.sendMessage( | ||
""" | ||
Hey there, sorry to tell you but unfortunately you have been kicked from the server %s. | ||
If you think this was a mistake, please contact a moderator or admin of the server. | ||
The reason for the kick is: %s | ||
""" | ||
.formatted(guild.getName(), reason))) | ||
.mapToResult() | ||
.map(Result::isSuccess); | ||
} | ||
|
||
private static AuditableRestAction<Void> kickUser(@NotNull Member target, | ||
@NotNull Member author, @NotNull String reason, @NotNull Guild guild) { | ||
logger.info("'{}' ({}) kicked the user '{}' ({}) from guild '{}' for reason '{}'.", | ||
author.getUser().getAsTag(), author.getId(), target.getUser().getAsTag(), | ||
target.getId(), guild.getName(), reason); | ||
|
||
return guild.kick(target, reason).reason(reason); | ||
} | ||
|
||
private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull Member target, | ||
@NotNull Member author, @NotNull String reason) { | ||
String dmNoticeText = ""; | ||
if (!hasSentDm) { | ||
dmNoticeText = "(Unable to send them a DM.)"; | ||
} | ||
return ModerationUtils.createActionResponse(author.getUser(), ModerationUtils.Action.KICK, | ||
target.getUser(), dmNoticeText, reason); | ||
} | ||
|
||
@SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"}) | ||
private boolean handleChecks(@NotNull Member bot, @NotNull Member author, | ||
@Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild, | ||
@NotNull Interaction event) { | ||
// Member doesn't exist if attempting to kick a user who is not part of the guild anymore. | ||
if (target == null) { | ||
handleAbsentTarget(event); | ||
return false; | ||
} | ||
if (!ModerationUtils.handleCanInteractWithTarget(ACTION_VERB, bot, author, target, event)) { | ||
return false; | ||
} | ||
if (!ModerationUtils.handleHasAuthorRole(ACTION_VERB, hasRequiredRole, author, event)) { | ||
return false; | ||
} | ||
if (!ModerationUtils.handleHasBotPermissions(ACTION_VERB, Permission.KICK_MEMBERS, bot, | ||
guild, event)) { | ||
return false; | ||
} | ||
if (!ModerationUtils.handleHasAuthorPermissions(ACTION_VERB, Permission.KICK_MEMBERS, | ||
author, guild, event)) { | ||
return false; | ||
} | ||
return ModerationUtils.handleReason(reason, event); | ||
} | ||
|
||
@Override | ||
public void onSlashCommand(@NotNull SlashCommandEvent event) { | ||
Member target = Objects.requireNonNull(event.getOption(TARGET_OPTION), "The target is null") | ||
.getAsMember(); | ||
Member author = Objects.requireNonNull(event.getMember(), "The author is null"); | ||
String reason = Objects.requireNonNull(event.getOption(REASON_OPTION), "The reason is null") | ||
.getAsString(); | ||
|
||
Guild guild = Objects.requireNonNull(event.getGuild()); | ||
Member bot = guild.getSelfMember(); | ||
|
||
if (!handleChecks(bot, author, target, reason, guild, event)) { | ||
return; | ||
} | ||
kickUserFlow(Objects.requireNonNull(target), author, reason, guild, event); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.