Skip to content

Migrate temporary ban/mute #267

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 14 commits into from
Dec 29, 2021
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.moderation.temp.TemporaryModerationRoutine;
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 @@ -46,6 +47,7 @@ public enum Commands {
// TODO This should be moved into some proper command system instead (see GH issue #235
// which adds support for routines)
new ModAuditLogRoutine(jda, database).start();
new TemporaryModerationRoutine(jda, actionsStore).start();

// TODO This should be moved into some proper command system instead (see GH issue #236
// which adds support for listeners)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
* @param reason the reason why this action was executed
*/
public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId, long authorId,
long targetId, @NotNull ModerationUtils.Action actionType,
@Nullable Instant actionExpiresAt, @NotNull String reason) {
long targetId, @NotNull ModerationAction actionType, @Nullable Instant actionExpiresAt,
@NotNull String reason) {

/**
* Creates the action record that corresponds to the given action entry from the database table.
Expand All @@ -34,7 +34,7 @@ public record ActionRecord(int caseId, @NotNull Instant issuedAt, long guildId,
static @NotNull ActionRecord of(@NotNull ModerationActionsRecord action) {
return new ActionRecord(action.getCaseId(), action.getIssuedAt(), action.getGuildId(),
action.getAuthorId(), action.getTargetId(),
ModerationUtils.Action.valueOf(action.getActionType()), action.getActionExpiresAt(),
ModerationAction.valueOf(action.getActionType()), action.getActionExpiresAt(),
action.getReason());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,12 @@ public AuditCommand(@NotNull ModerationActionsStore actionsStore) {
String shortSummary = "There are **%d actions** against the user.".formatted(actionAmount);

// Summary of all actions with their count, like "- Warn: 5", descending
Map<ModerationUtils.Action, Long> actionTypeToCount = actions.stream()
Map<ModerationAction, Long> actionTypeToCount = actions.stream()
.collect(Collectors.groupingBy(ActionRecord::actionType, Collectors.counting()));
String typeCountSummary = actionTypeToCount.entrySet()
.stream()
.filter(typeAndCount -> typeAndCount.getValue() > 0)
.sorted(Map.Entry.<ModerationUtils.Action, Long>comparingByValue().reversed())
.sorted(Map.Entry.<ModerationAction, Long>comparingByValue().reversed())
.map(typeAndCount -> "- **%s**: %d".formatted(typeAndCount.getKey(),
typeAndCount.getValue()))
.collect(Collectors.joining("\n"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import org.togetherjava.tjbot.commands.SlashCommandVisibility;
import org.togetherjava.tjbot.config.Config;

import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
Expand All @@ -38,10 +40,14 @@
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 DURATION_OPTION = "duration";
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";
@SuppressWarnings("StaticCollection")
private static final List<String> DURATIONS = List.of(ModerationUtils.PERMANENT_DURATION,
"1 hour", "3 hours", "1 day", "2 days", "3 days", "7 days", "30 days");
private final Predicate<String> hasRequiredRole;
private final ModerationActionsStore actionsStore;

Expand All @@ -53,7 +59,12 @@ public final class BanCommand extends SlashCommandAdapter {
public BanCommand(@NotNull ModerationActionsStore actionsStore) {
super(COMMAND_NAME, "Bans the given user from the server", SlashCommandVisibility.GUILD);

OptionData durationData = new OptionData(OptionType.STRING, DURATION_OPTION,
"the duration of the ban, permanent or temporary", true);
DURATIONS.forEach(duration -> durationData.addChoice(duration, duration));

getData().addOption(OptionType.USER, TARGET_OPTION, "The user who you want to ban", true)
.addOptions(durationData)
.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.",
Expand All @@ -75,53 +86,37 @@ private static RestAction<InteractionHook> handleAlreadyBanned(@NotNull Guild.Ba
return event.reply(message).setEphemeral(true);
}

@SuppressWarnings("MethodWithTooManyParameters")
private 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,
private static RestAction<Boolean> sendDm(@NotNull ISnowflake target,
@Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason,
@NotNull Guild guild, @NotNull GenericEvent event) {
String durationMessage =
temporaryData == null ? "permanently" : "for " + temporaryData.duration();
String dmMessage =
"""
Hey there, sorry to tell you but unfortunately you have been banned %s 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(durationMessage, guild.getName(), reason);

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)))
.flatMap(channel -> channel.sendMessage(dmMessage))
.mapToResult()
.map(Result::isSuccess);
}

private 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);

actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(),
ModerationUtils.Action.BAN, null, reason);

return guild.ban(target, deleteHistoryDays, reason);
}

private static @NotNull MessageEmbed sendFeedback(boolean hasSentDm, @NotNull User target,
@NotNull Member author, @NotNull String reason) {
@NotNull Member author, @Nullable ModerationUtils.TemporaryData temporaryData,
@NotNull String reason) {
String durationText = "The ban duration is: "
+ (temporaryData == null ? "permanent" : temporaryData.duration());
String dmNoticeText = "";
if (!hasSentDm) {
dmNoticeText = "(Unable to send them a DM.)";
dmNoticeText = "\n(Unable to send them a DM.)";
}
return ModerationUtils.createActionResponse(author.getUser(), ModerationUtils.Action.BAN,
target, dmNoticeText, reason);
return ModerationUtils.createActionResponse(author.getUser(), ModerationAction.BAN, target,
durationText + dmNoticeText, reason);
}

private static Optional<RestAction<InteractionHook>> handleNotAlreadyBannedResponse(
Expand All @@ -147,6 +142,35 @@ private static Optional<RestAction<InteractionHook>> handleNotAlreadyBannedRespo
.setEphemeral(true));
}

@SuppressWarnings("MethodWithTooManyParameters")
private RestAction<InteractionHook> banUserFlow(@NotNull User target, @NotNull Member author,
@Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason,
int deleteHistoryDays, @NotNull Guild guild, @NotNull SlashCommandEvent event) {
return sendDm(target, temporaryData, reason, guild, event)
.flatMap(hasSentDm -> banUser(target, author, temporaryData, reason, deleteHistoryDays,
guild).map(banResult -> hasSentDm))
.map(hasSentDm -> sendFeedback(hasSentDm, target, author, temporaryData, reason))
.flatMap(event::replyEmbeds);
}

@SuppressWarnings("MethodWithTooManyParameters")
private AuditableRestAction<Void> banUser(@NotNull User target, @NotNull Member author,
@Nullable ModerationUtils.TemporaryData temporaryData, @NotNull String reason,
int deleteHistoryDays, @NotNull Guild guild) {
String durationMessage =
temporaryData == null ? "permanently" : "for " + temporaryData.duration();
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(),
durationMessage, guild.getName(), deleteHistoryDays, reason);

Instant expiresAt = temporaryData == null ? null : temporaryData.expiresAt();
actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(),
ModerationAction.BAN, expiresAt, reason);

return guild.ban(target, deleteHistoryDays, reason);
}

@SuppressWarnings({"BooleanMethodNameMustStartWithQuestion", "MethodWithTooManyParameters"})
private boolean handleChecks(@NotNull Member bot, @NotNull Member author,
@Nullable Member target, @NotNull CharSequence reason, @NotNull Guild guild,
Expand Down Expand Up @@ -178,9 +202,14 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) {
Member author = Objects.requireNonNull(event.getMember(), "The author is null");
String reason = Objects.requireNonNull(event.getOption(REASON_OPTION), "The reason is null")
.getAsString();
String duration =
Objects.requireNonNull(event.getOption(DURATION_OPTION), "The duration is null")
.getAsString();

Guild guild = Objects.requireNonNull(event.getGuild());
Member bot = guild.getSelfMember();
Optional<ModerationUtils.TemporaryData> temporaryData =
ModerationUtils.computeTemporaryData(duration);

if (!handleChecks(bot, author, targetOption.getAsMember(), reason, guild, event)) {
return;
Expand All @@ -195,9 +224,10 @@ public void onSlashCommand(@NotNull SlashCommandEvent event) {
return handleAlreadyBanned(alreadyBanned.get(), event);
}

return handleNotAlreadyBannedResponse(Objects
.requireNonNull(alreadyBanned.getFailure()), event, guild, target).orElseGet(
() -> banUserFlow(target, author, reason, deleteHistoryDays, guild, event));
return handleNotAlreadyBannedResponse(
Objects.requireNonNull(alreadyBanned.getFailure()), event, guild, target)
.orElseGet(() -> banUserFlow(target, author, temporaryData.orElse(null),
reason, deleteHistoryDays, guild, event));
}).queue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ private AuditableRestAction<Void> kickUser(@NotNull Member target, @NotNull Memb
target.getId(), guild.getName(), reason);

actionsStore.addAction(guild.getIdLong(), author.getIdLong(), target.getIdLong(),
ModerationUtils.Action.KICK, null, reason);
ModerationAction.KICK, null, reason);

return guild.kick(target, reason).reason(reason);
}
Expand All @@ -103,7 +103,7 @@ private AuditableRestAction<Void> kickUser(@NotNull Member target, @NotNull Memb
if (!hasSentDm) {
dmNoticeText = "(Unable to send them a DM.)";
}
return ModerationUtils.createActionResponse(author.getUser(), ModerationUtils.Action.KICK,
return ModerationUtils.createActionResponse(author.getUser(), ModerationAction.KICK,
target.getUser(), dmNoticeText, reason);
}

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

import org.jetbrains.annotations.NotNull;

/**
* All available moderation actions.
*/
public enum ModerationAction {
/**
* When a user bans another user.
*/
BAN("banned"),
/**
* When a user unbans another user.
*/
UNBAN("unbanned"),
/**
* When a user kicks another user.
*/
KICK("kicked"),
/**
* When a user warns another user.
*/
WARN("warned"),
/**
* When a user mutes another user.
*/
MUTE("muted"),
/**
* When a user unmutes another user.
*/
UNMUTE("unmuted");

private final String verb;

/**
* Creates an instance with the given verb
*
* @param verb the verb of the action, as it would be used in a sentence, such as "banned" or
* "kicked"
*/
ModerationAction(@NotNull String verb) {
this.verb = verb;
}

/**
* Gets the verb of the action, as it would be used in a sentence.
* <p>
* Such as "banned" or "kicked"
*
* @return the verb of this action
*/
public @NotNull String getVerb() {
return verb;
}
}
Loading