Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,6 +71,7 @@ public static Collection<Feature> 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
Expand Down Expand Up @@ -95,7 +99,9 @@ public static Collection<Feature> 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));
Expand Down Expand Up @@ -132,6 +138,7 @@ public static Collection<Feature> 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));
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> isStagingChannelName;
private final Predicate<String> 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<CodeFence> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -34,17 +34,22 @@
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.
* <p>
* Messages can be registered by using {@link #addAndHandleCodeMessage(Message)}.
* <p>
* Code actions are automatically updated whenever the code in the original message is edited or
* deleted.
*/
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;
Expand Down Expand Up @@ -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<CodeFence> 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();
Expand All @@ -131,10 +130,21 @@ private MessageCreateData createCodeReplyMessage(long originalMessageId) {

private List<Button> createButtons(long originalMessageId,
@Nullable CodeAction currentlyActiveAction) {
return labelToCodeAction.values().stream().map(action -> {
Stream<Button> codeActionButtons = labelToCodeAction.values().stream().map(action -> {
Button button = createButtonForAction(action, originalMessageId);
return action == currentlyActiveAction ? button.asDisabled() : button;
}).toList();
});

Stream<Button> otherButtons = Stream.of(createDeleteButton(originalMessageId));

return Stream.concat(codeActionButtons, otherButtons).toList();
}

private Button createDeleteButton(long originalMessageId) {
String noCodeActionLabel = "";
return Button.danger(componentIdInteractor
.generateComponentId(Long.toString(originalMessageId), noCodeActionLabel, DELETE_CUE),
Emoji.fromUnicode("🗑"));
}

private Button createButtonForAction(CodeAction action, long originalMessageId) {
Expand All @@ -146,8 +156,13 @@ private Button createButtonForAction(CodeAction action, long originalMessageId)
@Override
public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
long originalMessageId = Long.parseLong(args.get(0));
CodeAction codeAction = getActionOfEvent(event);
// The third arg indicates a non-code-action button
if (args.size() >= 3 && DELETE_CUE.equals(args.get(2))) {
deleteCodeReply(event, originalMessageId);
return;
}

CodeAction codeAction = getActionOfEvent(event);
event.deferEdit().queue();

// User decided for an action, apply it to the code
Expand All @@ -166,23 +181,21 @@ public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
// since we have the context here, we can restore that information
originalMessageToCodeReply.put(originalMessageId, event.getMessageIdLong());

Optional<CodeFence> maybeCode =
MessageUtils.extractCode(originalMessage.get().getContentRaw());
if (maybeCode.isEmpty()) {
return event.getHook()
.sendMessage(
"Sorry, I am unable to locate any code in the original message, was it removed?")
.setEphemeral(true);
}
CodeFence code = extractCodeOrFallback(originalMessage.get().getContentRaw());

// Apply the selected action
return event.getHook()
.editOriginalEmbeds(codeAction.apply(maybeCode.orElseThrow()))
.editOriginalEmbeds(codeAction.apply(code))
.setActionRow(createButtons(originalMessageId, codeAction));
})
.queue();
}

private void deleteCodeReply(ButtonInteractionEvent event, long originalMessageId) {
originalMessageToCodeReply.invalidate(originalMessageId);
event.getMessage().delete().queue();
}

private CodeAction getActionOfEvent(ButtonInteractionEvent event) {
return labelToCodeAction.get(event.getButton().getLabel());
}
Expand All @@ -198,13 +211,7 @@ public void onMessageUpdated(MessageUpdateEvent event) {
}

// Edit the code reply as well by re-applying the current action
String content = event.getMessage().getContentRaw();

Optional<CodeFence> maybeCode = MessageUtils.extractCode(content);
if (maybeCode.isEmpty()) {
// The original message had code, but now the code was removed
return;
}
CodeFence code = extractCodeOrFallback(event.getMessage().getContentRaw());

event.getChannel().retrieveMessageById(codeReplyMessageId).flatMap(codeReplyMessage -> {
Optional<CodeAction> maybeCodeAction = getCurrentActionFromCodeReply(codeReplyMessage);
Expand All @@ -214,8 +221,7 @@ public void onMessageUpdated(MessageUpdateEvent event) {
}

// Re-apply the current action
return codeReplyMessage
.editMessageEmbeds(maybeCodeAction.orElseThrow().apply(maybeCode.orElseThrow()));
return codeReplyMessage.editMessageEmbeds(maybeCodeAction.orElseThrow().apply(code));
}).queue(any -> {
}, failure -> logger.warn(
"Attempted to update a code-reply-message ({}), but failed. The original code-message was {}",
Expand Down Expand Up @@ -243,11 +249,15 @@ public void onMessageDeleted(MessageDeleteEvent event) {
}

// Delete the code reply as well
originalMessageToCodeReply.invalidate(codeReplyMessageId);
originalMessageToCodeReply.invalidate(originalMessageId);

event.getChannel().deleteMessageById(codeReplyMessageId).queue(any -> {
}, failure -> logger.warn(
"Attempted to delete a code-reply-message ({}), but failed. The original code-message was {}",
codeReplyMessageId, originalMessageId, failure));
}

private static CodeFence extractCodeOrFallback(String content) {
return MessageUtils.extractCode(content).orElseGet(() -> new CodeFence("java", content));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.togetherjava.tjbot.commands.code;

import net.dv8tion.jda.api.events.interaction.command.MessageContextInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;

import org.togetherjava.tjbot.commands.BotCommandAdapter;
import org.togetherjava.tjbot.commands.CommandVisibility;
import org.togetherjava.tjbot.commands.MessageContextCommand;

/**
* Context command to allow users to select messages that contain code. They are then registered at
* {@link CodeMessageHandler} for further processing.
*/
public final class CodeMessageManualDetection extends BotCommandAdapter
implements MessageContextCommand {
private final CodeMessageHandler codeMessageHandler;

/**
* Creates a new instance.
*
* @param codeMessageHandler to register selected code messages at for further handling
*/
public CodeMessageManualDetection(CodeMessageHandler codeMessageHandler) {
super(Commands.message("code-actions"), CommandVisibility.GUILD);

this.codeMessageHandler = codeMessageHandler;
}

@Override
public void onMessageContext(MessageContextInteractionEvent event) {
event.reply("I registered the message as code-message, actions should appear now.")
.setEphemeral(true)
.queue();

codeMessageHandler.addAndHandleCodeMessage(event.getTarget());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ public final class AutoPruneHelperRoutine implements Routine {

private static final int ROLE_FULL_LIMIT = 100;
private static final int ROLE_FULL_THRESHOLD = 95;
private static final int PRUNE_MEMBER_AMOUNT = 10;
private static final int PRUNE_MEMBER_AMOUNT = 7;
private static final Period INACTIVE_AFTER = Period.ofDays(90);
private static final int RECENTLY_JOINED_DAYS = 7;
private static final int RECENTLY_JOINED_DAYS = 4;

private final HelpSystemHelper helper;
private final ModAuditLogWriter modAuditLogWriter;
Expand Down
Loading