Skip to content

Commit

Permalink
Educate about /foo when ?!.foo (#759)
Browse files Browse the repository at this point in the history
* React to message commands (!close, .close, ?close)

* matching
* advice content follows next

* Added actual advice

* javadoc, got rid of the massive Pattern.compile(".*") duplication

* unit tests

* Improved regex

* Test was unstable on CI/CD

* dot after smiley looks odd
  • Loading branch information
Zabuzard authored Feb 7, 2023
1 parent 8dc3b07 commit 28d173c
Show file tree
Hide file tree
Showing 13 changed files with 154 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@

import org.togetherjava.tjbot.config.Config;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.features.basic.PingCommand;
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
import org.togetherjava.tjbot.features.basic.SuggestionsUpDownVoter;
import org.togetherjava.tjbot.features.basic.VcActivityCommand;
import org.togetherjava.tjbot.features.basic.*;
import org.togetherjava.tjbot.features.bookmarks.*;
import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection;
import org.togetherjava.tjbot.features.code.CodeMessageHandler;
Expand Down Expand Up @@ -104,6 +101,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
features.add(codeMessageHandler);
features.add(new CodeMessageAutoDetection(config, codeMessageHandler));
features.add(new CodeMessageManualDetection(codeMessageHandler));
features.add(new SlashCommandEducator());

// Event receivers
features.add(new RejoinModerationRoleListener(actionsStore, config));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ public abstract class MessageReceiverAdapter implements MessageReceiver {

private final Pattern channelNamePattern;

/**
* Creates an instance of a message receiver, listening to messages of all channels.
*/
protected MessageReceiverAdapter() {
this(Pattern.compile(".*"));
}

/**
* Creates an instance of a message receiver with the given pattern.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.togetherjava.tjbot.features.basic;

import net.dv8tion.jda.api.EmbedBuilder;
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.MessageCreateAction;
import net.dv8tion.jda.api.utils.FileUpload;

import org.togetherjava.tjbot.features.MessageReceiverAdapter;
import org.togetherjava.tjbot.features.help.HelpSystemHelper;

import java.io.InputStream;
import java.util.function.Predicate;
import java.util.regex.Pattern;

/**
* Listens to messages that are likely supposed to be message commands, such as {@code !foo} and
* then educates the user about using slash commands, such as {@code /foo} instead.
*/
public final class SlashCommandEducator extends MessageReceiverAdapter {
private static final String SLASH_COMMAND_POPUP_ADVICE_PATH = "slashCommandPopupAdvice.png";
private static final Predicate<String> IS_MESSAGE_COMMAND = Pattern.compile("""
[.!?] #Start of message command
[a-zA-Z]{2,15} #Name of message command, e.g. 'close'
.* #Rest of the message
""", Pattern.COMMENTS).asMatchPredicate();

@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (event.getAuthor().isBot() || event.isWebhookMessage()) {
return;
}

String content = event.getMessage().getContentRaw();
if (IS_MESSAGE_COMMAND.test(content)) {
sendAdvice(event.getMessage());
}
}

private void sendAdvice(Message message) {
String content =
"""
Looks like you attempted to use a command? Please note that we only use **slash-commands** on this server 🙂
Try starting your message with a forward-slash `/` and Discord should open a popup showing you all available commands.
A command might then look like `/foo` 👍""";

createReply(message, content, SLASH_COMMAND_POPUP_ADVICE_PATH).queue();
}

private static MessageCreateAction createReply(Message messageToReplyTo, String content,
String imagePath) {
boolean useImage = true;
InputStream imageData = HelpSystemHelper.class.getResourceAsStream("/" + imagePath);
if (imageData == null) {
useImage = false;
}

MessageEmbed embed = new EmbedBuilder().setDescription(content)
.setImage(useImage ? "attachment://" + imagePath : null)
.build();

MessageCreateAction action = messageToReplyTo.replyEmbeds(embed);
if (useImage) {
action = action.addFiles(FileUpload.fromData(imageData, imagePath));
}

return action;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ public final class CodeMessageAutoDetection extends MessageReceiverAdapter {
* @param codeMessageHandler to register detected code messages at for further handling
*/
public CodeMessageAutoDetection(Config config, CodeMessageHandler codeMessageHandler) {
super(Pattern.compile(".*"));

this.codeMessageHandler = codeMessageHandler;

isHelpForumName =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import java.awt.Color;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -66,8 +65,6 @@ public final class CodeMessageHandler extends MessageReceiverAdapter implements
* Creates a new instance.
*/
public CodeMessageHandler() {
super(Pattern.compile(".*"));

componentIdInteractor = new ComponentIdInteractor(getInteractionType(), getName());

List<CodeAction> codeActions = List.of(new FormatCodeCommand());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ public class FileSharingMessageListener extends MessageReceiverAdapter implement
* @see org.togetherjava.tjbot.features.Features
*/
public FileSharingMessageListener(Config config) {
super(Pattern.compile(".*"));

gistApiKey = config.getGistApiKey();
isHelpForumName =
Pattern.compile(config.getHelpSystem().getHelpForumPattern()).asMatchPredicate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
import org.togetherjava.tjbot.features.moderation.modmail.ModMailCommand;
import org.togetherjava.tjbot.features.utils.MessageUtils;

import java.awt.*;
import java.awt.Color;
import java.util.List;
import java.util.Locale;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;

/**
* Reacts to blacklisted attachments being posted, upon which they are deleted.
Expand All @@ -35,7 +34,6 @@ public final class BlacklistedAttachmentListener extends MessageReceiverAdapter
* @param modAuditLogWriter to inform the mods about the suspicious attachment
*/
public BlacklistedAttachmentListener(Config config, ModAuditLogWriter modAuditLogWriter) {
super(Pattern.compile(".*"));
this.modAuditLogWriter = modAuditLogWriter;
blacklistedFileExtensions = config.getBlacklistedFileExtensions();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt
*/
public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHistoryStore,
Config config) {
super(Pattern.compile(".*"));

this.actionsStore = actionsStore;
this.scamHistoryStore = scamHistoryStore;
this.config = config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,6 @@ public final class TopHelpersMessageListener extends MessageReceiverAdapter {
* @param config the config to use for this
*/
public TopHelpersMessageListener(Database database, Config config) {
super(Pattern.compile(".*"));

this.database = database;

isHelpForumName =
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.togetherjava.tjbot.features.basic;

import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.ChannelType;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import org.togetherjava.tjbot.features.MessageReceiver;
import org.togetherjava.tjbot.jda.JdaTester;

import java.util.List;
import java.util.stream.Stream;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

final class SlashCommandEducatorTest {
private JdaTester jdaTester;
private MessageReceiver messageReceiver;

@BeforeEach
void setUp() {
jdaTester = new JdaTester();
messageReceiver = new SlashCommandEducator();
}

private MessageReceivedEvent sendMessage(String content) {
MessageCreateData message = new MessageCreateBuilder().setContent(content).build();
MessageReceivedEvent event =
jdaTester.createMessageReceiveEvent(message, List.of(), ChannelType.TEXT);

messageReceiver.onMessageReceived(event);

return event;
}

@ParameterizedTest
@MethodSource("provideMessageCommands")
void sendsAdviceOnMessageCommand(String message) {
// GIVEN a message containing a message command
// WHEN the message is sent
MessageReceivedEvent event = sendMessage(message);

// THEN the system replies to it with an advice
verify(event.getMessage(), times(1)).replyEmbeds(any(MessageEmbed.class));
}

@ParameterizedTest
@MethodSource("provideOtherMessages")
void ignoresOtherMessages(String message) {
// GIVEN a message that is not a message command
// WHEN the message is sent
MessageReceivedEvent event = sendMessage(message);

// THEN the system ignores the message and does not reply to it
verify(event.getMessage(), never()).replyEmbeds(any(MessageEmbed.class));
}

private static Stream<String> provideMessageCommands() {
return Stream.of("!foo", ".foo", "?foo", ".test", "!whatever", "!this is a test");
}

private static Stream<String> provideOtherMessages() {
return Stream.of(" a ", "foo", "#foo", "/foo", "!!!", "?!?!?", "?", ".,-", "!f", "! foo");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,6 @@ void reminderIsNotSendIfNotPending() {
private static void assertSimilar(Instant expected, Instant actual) {
// NOTE For some reason, the instant ends up in the database slightly wrong already (about
// half a second), seems to be an issue with jOOQ
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(1));
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(2));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ public JdaTester() {
doNothing().when(messageCreateAction).queue();
when(messageCreateAction.setContent(any())).thenReturn(messageCreateAction);
when(messageCreateAction.addContent(any())).thenReturn(messageCreateAction);
when(messageCreateAction.addFiles(any(FileUpload.class))).thenReturn(messageCreateAction);
when(messageCreateAction.addFiles(anyCollection())).thenReturn(messageCreateAction);

CacheRestAction<PrivateChannel> privateChannelAction =
createSucceededActionMock(privateChannel, CacheRestAction.class);
Expand Down

0 comments on commit 28d173c

Please sign in to comment.