Repository navigation
Added ThisIsAgainstRulesCommand (Scam and Job-Ad) (formerly ThisIsScam, only Scam)
#1567
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
base: develop
Are you sure you want to change the base?
Changes from all commits
1edf9cc
b1b8b59
9e34386
a2a6af7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,9 +14,9 @@ | |
| import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent; | ||
| import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; | ||
| import net.dv8tion.jda.api.exceptions.ErrorHandler; | ||
| import net.dv8tion.jda.api.interactions.InteractionHook; | ||
| import net.dv8tion.jda.api.interactions.commands.build.Commands; | ||
| import net.dv8tion.jda.api.interactions.components.buttons.Button; | ||
| import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle; | ||
| import net.dv8tion.jda.api.requests.ErrorResponse; | ||
| import net.dv8tion.jda.api.requests.RestAction; | ||
| import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; | ||
|
|
@@ -49,19 +49,29 @@ | |
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * Allows users to report a message as potential scam. Moderators can confirm the report from the | ||
| * audit log, causing the author to be quarantined plus message history getting deleted. | ||
| * Allows users to alert moderators about a message that violates the rules. Moderators then decide | ||
| * how to handle it: | ||
| * <ul> | ||
| * <li><b>Scam</b>: the author is quarantined, then banned and unbanned to delete their recent | ||
| * messages.</li> | ||
| * <li><b>Job-Ad</b>: the author is warned, then banned and unbanned to delete their recent | ||
| * messages.</li> | ||
| * <li><b>Dismiss</b>: nothing happens, the alert is closed.</li> | ||
| * </ul> | ||
| * The ban and unban only serves to delete the messages, the user is free to join back afterward. | ||
| */ | ||
| public final class ThisIsScamCommand extends BotCommandAdapter implements MessageContextCommand { | ||
| private static final Logger logger = LoggerFactory.getLogger(ThisIsScamCommand.class); | ||
|
|
||
| private static final String COMMAND_NAME = "this-is-scam"; | ||
| private static final String COMMAND_NAME = "this-is-against-rules"; | ||
|
|
||
| private static final String ACTION_TITLE = "Quarantine"; | ||
| private static final String ACTION_REASON = "Message was reported and confirmed as scam"; | ||
| private static final String SCAM_REASON = "Message was reported and confirmed as scam"; | ||
| private static final String JOB_AD_REASON = | ||
| "Message was reported and confirmed as a job advertisement"; | ||
|
|
||
| private static final String FAILED_MESSAGE = | ||
| "Sorry, there was an issue forwarding your scam report to the moderators. We are investigating."; | ||
| "Sorry, there was an issue forwarding your report to the moderators. We are investigating."; | ||
|
|
||
| private static final Duration USER_COMMAND_COOLDOWN = Duration.ofMinutes(1); | ||
|
|
||
| private final Config config; | ||
|
|
@@ -78,8 +88,8 @@ public final class ThisIsScamCommand extends BotCommandAdapter implements Messag | |
| /** | ||
| * Creates a new instance. | ||
| * | ||
| * @param config to resolve the moderation audit log channel and quarantined role | ||
| * @param actionsStore used to store issued quarantine actions | ||
| * @param config resolve the mod channel | ||
| * @param actionsStore used to store issued moderative actions | ||
| */ | ||
| public ThisIsScamCommand(Config config, ModerationActionsStore actionsStore) { | ||
| super(Commands.message(COMMAND_NAME), CommandVisibility.GUILD); | ||
|
|
@@ -98,16 +108,18 @@ public void onMessageContext(MessageContextInteractionEvent event) { | |
| return; | ||
| } | ||
|
|
||
| Optional<TextChannel> modAuditLog = findModMailChannel(event); | ||
| if (modAuditLog.isEmpty()) { | ||
| Message message = event.getTarget(); | ||
| Optional<TextChannel> modChannel = findModChannel(event); | ||
| if (modChannel.isEmpty()) { | ||
| reportedMessageToTimestamp.invalidate(message.getIdLong()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why that logic change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's an edge-case: I noticed an issue while doing the tests. I know the |
||
| event.reply(FAILED_MESSAGE).setEphemeral(true).queue(); | ||
| return; | ||
| } | ||
|
|
||
| Message message = event.getTarget(); | ||
| reportToMods(message, modAuditLog.orElseThrow()).mapToResult().map(result -> { | ||
| reportToMods(message, modChannel.orElseThrow()).mapToResult().map(result -> { | ||
| if (result.isFailure()) { | ||
| logger.warn("Unable to forward a scam report to the mod audit log channel.", | ||
| reportedMessageToTimestamp.invalidate(message.getIdLong()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why that logic change?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| logger.warn("Unable to forward a report to the mod mail channel.", | ||
| result.getFailure()); | ||
| return FAILED_MESSAGE; | ||
| } | ||
|
|
@@ -130,43 +142,39 @@ private boolean handleIsOnCooldown(MessageContextInteractionEvent event) { | |
| return false; | ||
| } | ||
|
|
||
| event.reply("You just reported a message as scam, please wait a bit.") | ||
| .setEphemeral(true) | ||
| .queue(); | ||
| event.reply("You just reported a message, please wait a bit.").setEphemeral(true).queue(); | ||
| return true; | ||
| } | ||
|
|
||
| private boolean handleWasAlreadyReportedMessage(MessageContextInteractionEvent event) { | ||
| long messageId = event.getTarget().getIdLong(); | ||
| if (reportedMessageToTimestamp.getIfPresent(messageId) != null) { | ||
| event.reply("This message was already reported as potential scam.") | ||
| .setEphemeral(true) | ||
| .queue(); | ||
| event.reply("This message was already reported.").setEphemeral(true).queue(); | ||
| return true; | ||
| } | ||
|
|
||
| reportedMessageToTimestamp.put(messageId, Instant.now()); | ||
| return false; | ||
| } | ||
|
|
||
| private Optional<TextChannel> findModMailChannel(MessageContextInteractionEvent event) { | ||
| private Optional<TextChannel> findModChannel(MessageContextInteractionEvent event) { | ||
| Guild guild = Objects.requireNonNull(event.getGuild()); | ||
| Optional<TextChannel> modMailChannel = Guilds.findTextChannel(guild, isModMailChannel); | ||
| if (modMailChannel.isEmpty()) { | ||
| logger.warn( | ||
| "Cannot find the designated mod audit log channel in guild '{}' with the pattern '{}'", | ||
| guild.getId(), config.getModAuditLogChannelPattern()); | ||
| "Cannot find the designated mod mail channel in guild '{}' with the pattern '{}'", | ||
| guild.getId(), config.getModMailChannelPattern()); | ||
| } | ||
| return modMailChannel; | ||
| } | ||
|
|
||
| private MessageCreateAction reportToMods(Message message, TextChannel auditChannel) { | ||
| private MessageCreateAction reportToMods(Message message, TextChannel modMailChannel) { | ||
| User author = message.getAuthor(); | ||
| String description = createDescription(message); | ||
| long accountAgeDays = ChronoUnit.DAYS.between(author.getTimeCreated(), | ||
| OffsetDateTime.now(ZoneOffset.UTC)); | ||
|
|
||
| MessageEmbed reportEmbed = new EmbedBuilder().setTitle("Is this Scam?") | ||
| MessageEmbed reportEmbed = new EmbedBuilder().setTitle("Rule Violation") | ||
| .setDescription( | ||
| MessageUtils.abbreviate(description, MessageEmbed.DESCRIPTION_MAX_LENGTH)) | ||
| .setAuthor(author.getName(), null, author.getEffectiveAvatarUrl()) | ||
|
|
@@ -175,13 +183,19 @@ private MessageCreateAction reportToMods(Message message, TextChannel auditChann | |
| .setFooter("%s - account age %d days".formatted(author.getId(), accountAgeDays)) | ||
| .build(); | ||
|
|
||
| long guildId = message.getGuild().getIdLong(); | ||
| long authorId = author.getIdLong(); | ||
| String[] args = {String.valueOf(guildId), String.valueOf(authorId)}; | ||
|
|
||
| return auditChannel.sendMessageEmbeds(reportEmbed) | ||
| .addActionRow(Button.success(generateComponentId(args), "Yes"), | ||
| Button.danger(generateComponentId(args), "No")); | ||
| String guildId = String.valueOf(message.getGuild().getIdLong()); | ||
| String authorId = String.valueOf(author.getIdLong()); | ||
|
|
||
| return modMailChannel.sendMessageEmbeds(reportEmbed) | ||
| .addActionRow( | ||
| Button.success(generateComponentId(guildId, authorId, ReportAction.SCAM.name()), | ||
| "Scam"), | ||
| Button.success( | ||
| generateComponentId(guildId, authorId, ReportAction.JOB_AD.name()), | ||
| "Job-Ad"), | ||
| Button.danger( | ||
| generateComponentId(guildId, authorId, ReportAction.DISMISS.name()), | ||
| "Dismiss")); | ||
| } | ||
|
|
||
| private static String createDescription(Message target) { | ||
|
|
@@ -209,29 +223,20 @@ private static String createDescription(Message target) { | |
| public void onButtonClick(ButtonInteractionEvent event, List<String> args) { | ||
| long guildId = Long.parseLong(args.get(0)); | ||
| long targetId = Long.parseLong(args.get(1)); | ||
| ReportAction action = ReportAction.valueOf(args.get(2)); | ||
|
|
||
| ButtonStyle clickedStyle = event.getButton().getStyle(); | ||
| boolean isScam = clickedStyle == ButtonStyle.SUCCESS; | ||
|
|
||
| MessageEmbed resultEmbed = new EmbedBuilder() | ||
| .setDescription( | ||
| isScam ? "This is scam. The user was quarantined and messages were deleted." | ||
| : "This is not scam, no action executed.") | ||
| .setColor(isScam ? Color.GREEN : Color.RED) | ||
| .build(); | ||
|
|
||
| List<MessageEmbed> embeds = new ArrayList<>(event.getMessage().getEmbeds()); | ||
| embeds.add(resultEmbed); | ||
|
|
||
| event.editMessageEmbeds(embeds).setComponents().queue(); | ||
| Member moderator = Objects.requireNonNull(event.getMember()); | ||
| List<MessageEmbed> existingEmbeds = new ArrayList<>(event.getMessage().getEmbeds()); | ||
|
|
||
| if (!isScam) { | ||
| if (action == ReportAction.DISMISS) { | ||
| event | ||
| .editMessageEmbeds(createResponseEmbeds(existingEmbeds, | ||
| "Dismissed, no action executed.", Color.RED)) | ||
| .setComponents() | ||
| .queue(); | ||
| return; | ||
| } | ||
|
|
||
| Guild guild = Objects.requireNonNull(event.getJDA().getGuildById(guildId)); | ||
| Member moderator = Objects.requireNonNull(event.getMember()); | ||
|
|
||
| ErrorHandler errorHandler = new ErrorHandler() | ||
| .handle(ErrorResponse.UNKNOWN_USER, failure -> logger.debug(LogMarkers.SENSITIVE, | ||
| "Attempted to handle user-reported scam, but user '{}' does not exist anymore.", | ||
|
|
@@ -240,41 +245,76 @@ public void onButtonClick(ButtonInteractionEvent event, List<String> args) { | |
| "Attempted to handle user-reported scam, but user '{}' is not a member of guild '{}' anymore.", | ||
| targetId, guildId)); | ||
|
|
||
| Guild guild = Objects.requireNonNull(event.getJDA().getGuildById(guildId)); | ||
|
|
||
| guild.retrieveMemberById(targetId) | ||
| .queue(target -> handleConfirmedScam(guild, target, moderator, event), errorHandler); | ||
| .queue(target -> handleConfirmedViolation(event, action, guild, moderator, target, | ||
| existingEmbeds), errorHandler); | ||
| } | ||
|
|
||
| private void handleConfirmedScam(Guild guild, Member target, Member moderator, | ||
| ButtonInteractionEvent event) { | ||
| if (!handleCanQuarantineAndBan(guild, target, event)) { | ||
| private void handleConfirmedViolation(ButtonInteractionEvent event, ReportAction action, | ||
| Guild guild, Member moderator, Member target, List<MessageEmbed> alertEmbeds) { | ||
|
|
||
| Member bot = guild.getSelfMember(); | ||
|
|
||
| boolean canExecute = switch (action) { | ||
| case SCAM -> { | ||
| Role quarantinedRole = | ||
| ModerationUtils.getQuarantinedRole(guild, config).orElse(null); | ||
| yield ModerationUtils.handleRoleChangeChecks(quarantinedRole, "quarantine", target, | ||
| bot, moderator, guild, SCAM_REASON, event); | ||
| } | ||
| case JOB_AD -> | ||
| ModerationUtils.handleCanInteractWithTarget("ban", bot, moderator, target, event); | ||
| default -> false; | ||
| }; | ||
|
|
||
| canExecute &= ModerationUtils.handleHasBotPermissions("ban", Permission.BAN_MEMBERS, bot, | ||
| guild, event); | ||
|
|
||
| if (!canExecute) { | ||
| logger.warn(LogMarkers.SENSITIVE, | ||
| "Moderator '{}' tried to execute report for user '{}' in guild '{}', but checks failed.", | ||
| moderator.getId(), target.getId(), guild.getId()); | ||
| return; | ||
| } | ||
|
|
||
| Consumer<? super Void> onSuccess = _ -> { | ||
| String successText; | ||
| RestAction<Void> handling = switch (action) { | ||
| case SCAM -> { | ||
| successText = "This is scam. The user was quarantined and messages were deleted."; | ||
| yield quarantineAndRemove(guild, target, moderator); | ||
| } | ||
| case JOB_AD -> { | ||
| successText = "This is a job-ad. The user was warned and messages were deleted."; | ||
| yield warnAndRemove(guild, target, moderator); | ||
| } | ||
| default -> throw new IllegalStateException("Unknown action provided: " + action); | ||
| }; | ||
| Consumer<? super Throwable> onFailure = failure -> logger.warn(LogMarkers.SENSITIVE, | ||
| "Failed to finish user-reported scam handling for user '{}' in guild '{}'.", | ||
| target.getId(), guild.getId(), failure); | ||
|
|
||
| sendQuarantineDm(target.getUser(), guild) | ||
| .flatMap(hasSentDm -> quarantineUser(guild, target, moderator)) | ||
| .flatMap(_ -> deleteMessagesByBanAndUnban(guild, target.getUser())) | ||
| .queue(onSuccess, onFailure); | ||
| } | ||
|
|
||
| private boolean handleCanQuarantineAndBan(Guild guild, Member target, | ||
| ButtonInteractionEvent event) { | ||
| Member bot = guild.getSelfMember(); | ||
| Member moderator = Objects.requireNonNull(event.getMember()); | ||
| Role quarantinedRole = ModerationUtils.getQuarantinedRole(guild, config).orElse(null); | ||
|
|
||
| return ModerationUtils.handleRoleChangeChecks(quarantinedRole, "quarantine", target, bot, | ||
| moderator, guild, ACTION_REASON, event) | ||
| && ModerationUtils.handleHasBotPermissions("ban", Permission.BAN_MEMBERS, bot, | ||
| guild, event); | ||
| String pendingText = "%s is handling this alert...".formatted(moderator.getAsMention()); | ||
|
|
||
| event.editMessageEmbeds(createResponseEmbeds(alertEmbeds, pendingText, Color.ORANGE)) | ||
| .setComponents() | ||
| .queue(hook -> { | ||
| Consumer<Void> onSuccess = ignored -> showResult(hook, alertEmbeds, | ||
| "%s (handled by %s)".formatted(successText, moderator.getAsMention()), | ||
| Color.GREEN); | ||
|
|
||
| Consumer<Throwable> onFailure = failure -> { | ||
| logger.warn(LogMarkers.SENSITIVE, | ||
| "Failed to handle alert for user '{}' in guild '{}'.", target.getId(), | ||
| guild.getId(), failure); | ||
| showResult(hook, alertEmbeds, | ||
| "Handling this alert failed, please check the logs and handle the user manually.", | ||
| Color.RED); | ||
| }; | ||
|
|
||
| handling.queue(onSuccess, onFailure); | ||
| }); | ||
| } | ||
|
|
||
| private RestAction<Boolean> sendQuarantineDm(User target, Guild guild) { | ||
| private RestAction<Void> quarantineAndRemove(Guild guild, Member target, Member moderator) { | ||
| String description = | ||
| """ | ||
| Hey there, sorry to tell you but unfortunately you have been put under quarantine after sending scam. | ||
|
|
@@ -284,28 +324,71 @@ private RestAction<Boolean> sendQuarantineDm(User target, Guild guild) { | |
| After you regained control of your account make sure you secure it properly, for example by using 2FA. | ||
| You can then join back our server and contact the mods to get unquarantined."""; | ||
|
|
||
| return ModerationUtils.sendModActionDm(ModerationUtils.getModActionEmbed(guild, | ||
| ACTION_TITLE, description, ACTION_REASON, true), target); | ||
| return sendDm(guild, target.getUser(), "Quarantine", description, SCAM_REASON) | ||
| .flatMap(_ -> { | ||
| recordAction(guild, target, moderator, ModerationAction.QUARANTINE, SCAM_REASON); | ||
| return guild | ||
| .addRoleToMember(target, | ||
| ModerationUtils.getQuarantinedRole(guild, config).orElseThrow()) | ||
| .reason(SCAM_REASON); | ||
| }) | ||
| .flatMap(_ -> deleteMessagesByBanThenUnban(guild, target.getUser(), SCAM_REASON)); | ||
| } | ||
|
|
||
| private RestAction<Void> quarantineUser(Guild guild, Member target, Member moderator) { | ||
| logger.info(LogMarkers.SENSITIVE, | ||
| "'{}' ({}) quarantined the user '{}' ({}) in guild '{}' for reason '{}'.", | ||
| moderator.getUser().getName(), moderator.getId(), target.getUser().getName(), | ||
| target.getId(), guild.getName(), ACTION_REASON); | ||
| private RestAction<Void> warnAndRemove(Guild guild, Member target, Member moderator) { | ||
| String description = | ||
| """ | ||
| Hey there, sorry to tell you but unfortunately you have been warned in the server for posting a job advertisement. | ||
| To delete your messages you were briefly removed from the server, you are welcome to join back."""; | ||
|
Comment on lines
+341
to
+342
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. im not super happy with the wording of this message. but we can talk about that at the very end when other stuff is fixed |
||
|
|
||
| return sendDm(guild, target.getUser(), "Warning", description, JOB_AD_REASON).flatMap(_ -> { | ||
| recordAction(guild, target, moderator, ModerationAction.WARN, JOB_AD_REASON); | ||
| return deleteMessagesByBanThenUnban(guild, target.getUser(), JOB_AD_REASON); | ||
| }).flatMap(_ -> deleteMessagesByBanThenUnban(guild, target.getUser(), JOB_AD_REASON)); | ||
| } | ||
|
|
||
| actionsStore.addAction(guild.getIdLong(), moderator.getIdLong(), target.getIdLong(), | ||
| ModerationAction.QUARANTINE, null, ACTION_REASON); | ||
| private static RestAction<Boolean> sendDm(Guild guild, User target, String title, | ||
| String description, String reason) { | ||
| return ModerationUtils.sendModActionDm( | ||
| ModerationUtils.getModActionEmbed(guild, title, description, reason, true), target); | ||
| } | ||
|
|
||
| private void recordAction(Guild guild, Member target, Member moderator, ModerationAction action, | ||
| String reason) { | ||
| logger.info(LogMarkers.SENSITIVE, | ||
| "'{}' ({}) executed '{}' against the user '{}' ({}) in guild '{}' for reason '{}'.", | ||
| moderator.getUser().getName(), moderator.getId(), action, | ||
| target.getUser().getName(), target.getId(), guild.getName(), reason); | ||
|
|
||
| return guild | ||
| .addRoleToMember(target, | ||
| ModerationUtils.getQuarantinedRole(guild, config).orElseThrow()) | ||
| .reason(ACTION_REASON); | ||
| actionsStore.addAction(guild.getIdLong(), moderator.getIdLong(), target.getIdLong(), action, | ||
| null, reason); | ||
| } | ||
|
|
||
| private RestAction<Void> deleteMessagesByBanAndUnban(Guild guild, User target) { | ||
| private static RestAction<Void> deleteMessagesByBanThenUnban(Guild guild, User target, | ||
| String reason) { | ||
| return guild.ban(target, 1, TimeUnit.DAYS) | ||
| .reason(ACTION_REASON) | ||
| .flatMap(_ -> guild.unban(target).reason(ACTION_REASON)); | ||
| .reason(reason) | ||
| .flatMap(_ -> guild.unban(target).reason(reason)); | ||
| } | ||
|
|
||
| private static List<MessageEmbed> createResponseEmbeds(List<MessageEmbed> stagedEmbeds, | ||
| String text, Color color) { | ||
| MessageEmbed resultEmbed = new EmbedBuilder().setDescription(text).setColor(color).build(); | ||
|
|
||
| List<MessageEmbed> embeds = new ArrayList<>(stagedEmbeds); | ||
| embeds.add(resultEmbed); | ||
|
|
||
| return embeds; | ||
| } | ||
|
|
||
| private static void showResult(InteractionHook hook, List<MessageEmbed> alertEmbeds, | ||
| String text, Color color) { | ||
| hook.editOriginalEmbeds(createResponseEmbeds(alertEmbeds, text, color)).queue(); | ||
| } | ||
|
|
||
| private enum ReportAction { | ||
| SCAM, | ||
| JOB_AD, | ||
| DISMISS | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(rename the file,
ThisIsAgainstRulesCommand)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do it at the end