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
Open
firasrg
wants to merge
6
commits into
develop
Choose a base branch
from
firasrg/feature/new-command-rule-violation-alert
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
1edf9cc
Extend the this-is-scam action command to handle scams and job ads
dependabot[bot] b1b8b59
Rolling back build.gradle (was added by mistake)
firasrg 9e34386
Applies adjustments and changes for CR1
firasrg a2a6af7
CR1: remove the custom generateComponentId method;
firasrg a1cf7e8
CR1: remove showResult()
firasrg e24ecfc
CR1: refactor onMessageContext() for better cache management;
firasrg 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,6 @@ | |
| import net.dv8tion.jda.api.exceptions.ErrorHandler; | ||
| 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 +48,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 +87,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); | ||
|
|
@@ -94,20 +103,21 @@ public void onMessageContext(MessageContextInteractionEvent event) { | |
| if (handleIsOnCooldown(event)) { | ||
| return; | ||
| } | ||
| if (handleWasAlreadyReportedMessage(event)) { | ||
|
|
||
| Optional<TextChannel> modChannel = findModChannel(event); | ||
| if (modChannel.isEmpty()) { | ||
| event.reply(FAILED_MESSAGE).setEphemeral(true).queue(); | ||
| return; | ||
| } | ||
|
|
||
| Optional<TextChannel> modAuditLog = findModMailChannel(event); | ||
| if (modAuditLog.isEmpty()) { | ||
| event.reply(FAILED_MESSAGE).setEphemeral(true).queue(); | ||
| if (handleWasAlreadyReportedMessage(event)) { | ||
| 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.", | ||
| logger.warn("Unable to forward a report to the mod mail channel.", | ||
| result.getFailure()); | ||
| return FAILED_MESSAGE; | ||
| } | ||
|
|
@@ -130,43 +140,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 +181,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 +221,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 +243,80 @@ 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 -> hook | ||
| .editOriginalEmbeds(createResponseEmbeds(alertEmbeds, | ||
| "%s (handled by %s)".formatted(successText, | ||
| moderator.getAsMention()), | ||
| Color.GREEN)) | ||
| .queue(); | ||
|
|
||
| Consumer<Throwable> onFailure = failure -> { | ||
| logger.warn(LogMarkers.SENSITIVE, | ||
| "Failed to handle alert for user '{}' in guild '{}'.", target.getId(), | ||
| guild.getId(), failure); | ||
| hook.editOriginalEmbeds(createResponseEmbeds(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 +326,66 @@ 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
+343
to
+344
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 |
||
|
|
||
| actionsStore.addAction(guild.getIdLong(), moderator.getIdLong(), target.getIdLong(), | ||
| ModerationAction.QUARANTINE, null, ACTION_REASON); | ||
| 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)); | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| return guild | ||
| .addRoleToMember(target, | ||
| ModerationUtils.getQuarantinedRole(guild, config).orElseThrow()) | ||
| .reason(ACTION_REASON); | ||
| 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); | ||
|
|
||
| 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 enum ReportAction { | ||
| SCAM, | ||
| JOB_AD, | ||
| DISMISS | ||
| } | ||
| } | ||
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.
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