diff --git a/application/build.gradle b/application/build.gradle index 23075c96f7..1d2426892e 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -61,10 +61,10 @@ dependencies { implementation 'org.scilab.forge:jlatexmath-font-greek:1.0.7' implementation 'org.scilab.forge:jlatexmath-font-cyrillic:1.0.7' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.14.0' - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0' - implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0' + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-csv:$jacksonVersion" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:$jacksonVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" implementation 'com.github.freva:ascii-table:1.8.0' 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 853b5be019..76cdd928b7 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/Features.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/Features.java @@ -6,13 +6,16 @@ import org.togetherjava.tjbot.commands.basic.RoleSelectCommand; import org.togetherjava.tjbot.commands.basic.SuggestionsUpDownVoter; import org.togetherjava.tjbot.commands.basic.VcActivityCommand; +import org.togetherjava.tjbot.commands.code.CodeMessageAutoDetection; import org.togetherjava.tjbot.commands.code.CodeMessageHandler; +import org.togetherjava.tjbot.commands.code.CodeMessageManualDetection; import org.togetherjava.tjbot.commands.filesharing.FileSharingMessageListener; import org.togetherjava.tjbot.commands.help.*; import org.togetherjava.tjbot.commands.mathcommands.TeXCommand; 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.ReportCommand; import org.togetherjava.tjbot.commands.moderation.attachment.BlacklistedAttachmentListener; import org.togetherjava.tjbot.commands.moderation.modmail.ModMailCommand; import org.togetherjava.tjbot.commands.moderation.scam.ScamBlocker; @@ -68,6 +71,7 @@ public static Collection createFeatures(JDA jda, Database database, Con ModAuditLogWriter modAuditLogWriter = new ModAuditLogWriter(config); ScamHistoryStore scamHistoryStore = new ScamHistoryStore(database); HelpSystemHelper helpSystemHelper = new HelpSystemHelper(jda, config, database); + CodeMessageHandler codeMessageHandler = new CodeMessageHandler(); // NOTE The system can add special system relevant commands also by itself, // hence this list may not necessarily represent the full list of all commands actually @@ -95,7 +99,9 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new MediaOnlyChannelListener(config)); features.add(new FileSharingMessageListener(config)); features.add(new BlacklistedAttachmentListener(config, modAuditLogWriter)); - features.add(new CodeMessageHandler()); + features.add(codeMessageHandler); + features.add(new CodeMessageAutoDetection(config, codeMessageHandler)); + features.add(new CodeMessageManualDetection(codeMessageHandler)); // Event receivers features.add(new RejoinModerationRoleListener(actionsStore, config)); @@ -132,6 +138,7 @@ public static Collection createFeatures(JDA jda, Database database, Con features.add(new AskCommand(config, helpSystemHelper, database)); features.add(new ModMailCommand(jda, config)); features.add(new HelpThreadCommand(config, helpSystemHelper)); + features.add(new ReportCommand(config)); // Mixtures features.add(new HelpThreadOverviewUpdater(config, helpSystemHelper)); diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageAutoDetection.java b/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageAutoDetection.java new file mode 100644 index 0000000000..d8adecc0d1 --- /dev/null +++ b/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageAutoDetection.java @@ -0,0 +1,79 @@ +package org.togetherjava.tjbot.commands.code; + +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.ChannelType; +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; + +import org.togetherjava.tjbot.commands.MessageReceiverAdapter; +import org.togetherjava.tjbot.commands.utils.CodeFence; +import org.togetherjava.tjbot.commands.utils.MessageUtils; +import org.togetherjava.tjbot.config.Config; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * Automatically detects messages that contain code and registers them at {@link CodeMessageHandler} + * for further processing. + */ +public final class CodeMessageAutoDetection extends MessageReceiverAdapter { + private static final long MINIMUM_LINES_OF_CODE = 3; + + private final CodeMessageHandler codeMessageHandler; + + private final Predicate isStagingChannelName; + private final Predicate isOverviewChannelName; + + /** + * Creates a new instance. + * + * @param config to figure out whether a message is from a help thread + * @param codeMessageHandler to register detected code messages at for further handling + */ + public CodeMessageAutoDetection(Config config, CodeMessageHandler codeMessageHandler) { + super(Pattern.compile(".*")); + + this.codeMessageHandler = codeMessageHandler; + + isStagingChannelName = Pattern.compile(config.getHelpSystem().getStagingChannelPattern()) + .asMatchPredicate(); + isOverviewChannelName = Pattern.compile(config.getHelpSystem().getOverviewChannelPattern()) + .asMatchPredicate(); + } + + @Override + public void onMessageReceived(MessageReceivedEvent event) { + if (event.isWebhookMessage() || event.getAuthor().isBot() || !isHelpThread(event)) { + return; + } + + Message originalMessage = event.getMessage(); + + Optional maybeCode = MessageUtils.extractCode(originalMessage.getContentRaw()); + if (maybeCode.isEmpty()) { + // There is no code in the message, ignore it + return; + } + + long amountOfCodeLines = + maybeCode.orElseThrow().code().lines().limit(MINIMUM_LINES_OF_CODE).count(); + if (amountOfCodeLines < MINIMUM_LINES_OF_CODE) { + return; + } + + codeMessageHandler.addAndHandleCodeMessage(originalMessage); + } + + private boolean isHelpThread(MessageReceivedEvent event) { + if (event.getChannelType() != ChannelType.GUILD_PUBLIC_THREAD) { + return false; + } + + ThreadChannel thread = event.getChannel().asThreadChannel(); + String rootChannelName = thread.getParentChannel().getName(); + return isStagingChannelName.test(rootChannelName) + || isOverviewChannelName.test(rootChannelName); + } +} diff --git a/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageHandler.java b/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageHandler.java index 9b087bf39a..a45a5c0f80 100644 --- a/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageHandler.java +++ b/application/src/main/java/org/togetherjava/tjbot/commands/code/CodeMessageHandler.java @@ -3,11 +3,11 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.emoji.Emoji; import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; import net.dv8tion.jda.api.events.message.MessageDeleteEvent; -import net.dv8tion.jda.api.events.message.MessageReceivedEvent; import net.dv8tion.jda.api.events.message.MessageUpdateEvent; import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; @@ -34,10 +34,13 @@ import java.util.function.Function; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; /** - * Handler that detects code in messages and offers code actions to the user, such as formatting - * their code. + * Handles code in registered messages and offers code actions to the user, such as formatting their + * code. + *

+ * Messages can be registered by using {@link #addAndHandleCodeMessage(Message)}. *

* Code actions are automatically updated whenever the code in the original message is edited or * deleted. @@ -45,6 +48,8 @@ public final class CodeMessageHandler extends MessageReceiverAdapter implements UserInteractor { private static final Logger logger = LoggerFactory.getLogger(CodeMessageHandler.class); + private static final String DELETE_CUE = "delete"; + static final Color AMBIENT_COLOR = Color.decode("#FDFD96"); private final ComponentIdInteractor componentIdInteractor; @@ -101,23 +106,17 @@ public void acceptComponentIdGenerator(ComponentIdGenerator generator) { componentIdInteractor.acceptComponentIdGenerator(generator); } - @Override - public void onMessageReceived(MessageReceivedEvent event) { - if (event.isWebhookMessage() || event.getAuthor().isBot()) { - return; - } - - Message originalMessage = event.getMessage(); - String content = originalMessage.getContentRaw(); - - Optional maybeCode = MessageUtils.extractCode(content); - if (maybeCode.isEmpty()) { - // There is no code in the message, ignore it - return; - } - + /** + * Adds the given message to the code messages handled by this instance. Also sends the + * corresponding code-reply to the author. + * + * @param originalMessage the code message to add to this handler + */ + public void addAndHandleCodeMessage(Message originalMessage) { // Suggest code actions and remember the message <-> reply - originalMessage.reply(createCodeReplyMessage(originalMessage.getIdLong())) + MessageCreateData codeReply = createCodeReplyMessage(originalMessage.getIdLong()); + + originalMessage.reply(codeReply) .onSuccess(replyMessage -> originalMessageToCodeReply.put(originalMessage.getIdLong(), replyMessage.getIdLong())) .queue(); @@ -131,10 +130,21 @@ private MessageCreateData createCodeReplyMessage(long originalMessageId) { private List