diff --git a/application/config.json.template b/application/config.json.template index 3a34d248fc..5fc157fbd3 100644 --- a/application/config.json.template +++ b/application/config.json.template @@ -45,5 +45,43 @@ ], "categoryRoleSuffix": " - Helper" }, - "mediaOnlyChannelPattern": "memes" + "mediaOnlyChannelPattern": "memes", + "blacklistedFileExtension": [ + "application", + "bat", + "cmd", + "com", + "cpl", + "exe", + "gadget", + "hta", + "inf" + "jse", + "lnk", + "msc", + "msh", + "msh1", + "msh1xml", + "msh2", + "msh2xml", + "mshxml", + "msi", + "msp", + "pif", + "ps1", + "ps1xml", + "ps2", + "ps2xml", + "psc1", + "psc2", + "scf", + "scr", + "vb", + "vbe", + "vbs", + "ws", + "wsc", + "wsf", + "wsh" + ] } diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java index 0826682c08..d2c07ae7da 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -11,6 +11,7 @@ import org.togetherjava.tjbot.commands.mathcommands.wolframalpha.WolframAlphaCommand; import org.togetherjava.tjbot.commands.mediaonly.MediaOnlyChannelListener; import org.togetherjava.tjbot.commands.moderation.*; +import org.togetherjava.tjbot.commands.moderation.attachment.BlacklistedAttachmentListener; import org.togetherjava.tjbot.commands.moderation.scam.ScamBlocker; import org.togetherjava.tjbot.commands.moderation.scam.ScamHistoryPurgeRoutine; import org.togetherjava.tjbot.commands.moderation.scam.ScamHistoryStore; @@ -92,6 +93,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new ImplicitAskListener(config, helpSystemHelper)); features.add(new MediaOnlyChannelListener(config)); features.add(new FileSharingMessageListener(config)); + features.add(new BlacklistedAttachmentListener(config, modAuditLogWriter)); // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java new file mode 100644 index 0000000000..5fbc6cd0a2 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/moderation/attachment/BlacklistedAttachmentListener.java @@ -0,0 +1,118 @@ +package org.togetherjava.tjbot.commands.moderation.attachment; + +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.requests.RestAction; +import org.togetherjava.tjbot.commands.MessageReceiverAdapter; +import org.togetherjava.tjbot.config.Config; +import org.togetherjava.tjbot.moderation.ModAuditLogWriter; + +import javax.annotation.Nonnull; +import java.awt.*; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Reacts to blacklisted attachments being posted, upon which they are deleted. + */ +public final class BlacklistedAttachmentListener extends MessageReceiverAdapter { + private final ModAuditLogWriter modAuditLogWriter; + private final List blacklistedFileExtensions; + + /** + * Creates a AttachmentListener to receive all message sent in any channel. + * + * @param config to find the blacklisted media attachments + * @param modAuditLogWriter to inform the mods about the suspicious attachment + */ + public BlacklistedAttachmentListener(Config config, ModAuditLogWriter modAuditLogWriter) { + super(Pattern.compile(".*")); + this.modAuditLogWriter = modAuditLogWriter; + this.blacklistedFileExtensions = config.getBlacklistedFileExtensions(); + } + + @Override + public void onMessageReceived(MessageReceivedEvent event) { + if (event.getAuthor().isBot() || event.isWebhookMessage()) { + return; + } + if (doesMessageContainBlacklistedContent(event.getMessage())) { + handleBadMessage(event.getMessage()); + } + } + + private void handleBadMessage(Message message) { + message.delete().flatMap(any -> dmUser(message)).queue(any -> warnMods(message)); + } + + @Nonnull + private RestAction dmUser(Message message) { + Message dmMessage = createDmMessage(message); + return message.getAuthor() + .openPrivateChannel() + .flatMap(privateChannel -> privateChannel.sendMessage(dmMessage)); + } + + @Nonnull + private Message createDmMessage(Message originalMessage) { + String contentRaw = originalMessage.getContentRaw(); + String blacklistedAttachments = + String.join(", ", getBlacklistedAttachmentsFromMessage(originalMessage)); + + String dmMessageContent = + """ + Hey there, you posted a message containing a blacklisted file attachment: %s. + We had to delete your message for security reasons. + + Feel free to repost your message without, or with a different file instead. Sorry for any inconvenience caused by this 🙇️ + """ + .formatted(blacklistedAttachments); + + // No embed needed if there was no message from the user + if (contentRaw.isEmpty()) { + return new MessageBuilder(dmMessageContent).build(); + } + return createBaseResponse(contentRaw, dmMessageContent); + } + + @Nonnull + private Message createBaseResponse(String originalMessageContent, String dmMessageContent) { + MessageEmbed originalMessageEmbed = + new EmbedBuilder().setDescription(originalMessageContent) + .setColor(Color.ORANGE) + .build(); + return new MessageBuilder(dmMessageContent).setEmbeds(originalMessageEmbed).build(); + } + + @Nonnull + private List getBlacklistedAttachmentsFromMessage(Message originalMessage) { + return originalMessage.getAttachments() + .stream() + .filter(attachment -> blacklistedFileExtensions + .contains(attachment.getFileExtension().toLowerCase(Locale.US))) + .map(Message.Attachment::getFileName) + .toList(); + } + + private boolean doesMessageContainBlacklistedContent(Message message) { + return message.getAttachments() + .stream() + .anyMatch(attachment -> blacklistedFileExtensions + .contains(attachment.getFileExtension().toLowerCase(Locale.US))); + } + + private void warnMods(Message sentUserMessage) { + String blacklistedAttachmentsFromMessage = + String.join(", ", getBlacklistedAttachmentsFromMessage(sentUserMessage)); + + modAuditLogWriter.write( + "Message with blacklisted content detected: %s" + .formatted(blacklistedAttachmentsFromMessage), + "Sent Message: %s".formatted(sentUserMessage), sentUserMessage.getAuthor(), + sentUserMessage.getTimeCreated(), sentUserMessage.getGuild()); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/config/Config.java b/application/src/main/java/org/togetherjava/tjbot/config/Config.java index 6bc43ec68e..116799262a 100644 --- a/application/src/main/java/org/togetherjava/tjbot/config/Config.java +++ b/application/src/main/java/org/togetherjava/tjbot/config/Config.java @@ -8,6 +8,8 @@ import javax.annotation.Nonnull; import java.io.IOException; import java.nio.file.Path; +import java.util.Collections; +import java.util.List; /** * Configuration of the application. Create instances using {@link #load(Path)}. @@ -28,6 +30,7 @@ public final class Config { private final ScamBlockerConfig scamBlocker; private final String wolframAlphaAppId; private final HelpSystemConfig helpSystem; + private final List blacklistedFileExtension; private final String mediaOnlyChannelPattern; @@ -48,7 +51,8 @@ private Config(@JsonProperty("token") String token, @JsonProperty("scamBlocker") ScamBlockerConfig scamBlocker, @JsonProperty("wolframAlphaAppId") String wolframAlphaAppId, @JsonProperty("helpSystem") HelpSystemConfig helpSystem, - @JsonProperty("mediaOnlyChannelPattern") String mediaOnlyChannelPattern) { + @JsonProperty("mediaOnlyChannelPattern") String mediaOnlyChannelPattern, + @JsonProperty("blacklistedFileExtension") List blacklistedFileExtension) { this.token = token; this.gistApiKey = gistApiKey; this.databasePath = databasePath; @@ -65,6 +69,7 @@ private Config(@JsonProperty("token") String token, this.wolframAlphaAppId = wolframAlphaAppId; this.helpSystem = helpSystem; this.mediaOnlyChannelPattern = mediaOnlyChannelPattern; + this.blacklistedFileExtension = blacklistedFileExtension; } /** @@ -235,4 +240,14 @@ public HelpSystemConfig getHelpSystem() { public String getMediaOnlyChannelPattern() { return mediaOnlyChannelPattern; } + + /** + * Gets a list of all blacklisted file extensions. + * + * @return a list of all blacklisted file extensions + */ + @Nonnull + public List getBlacklistedFileExtensions() { + return Collections.unmodifiableList(blacklistedFileExtension); + } }