Skip to content

Commit 28d173c

Browse files
authored
Educate about /foo when ?!.foo (#759)
* 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
1 parent 8dc3b07 commit 28d173c

File tree

13 files changed

+154
-19
lines changed

13 files changed

+154
-19
lines changed

application/src/main/java/org/togetherjava/tjbot/features/Features.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44

55
import org.togetherjava.tjbot.config.Config;
66
import org.togetherjava.tjbot.db.Database;
7-
import org.togetherjava.tjbot.features.basic.PingCommand;
8-
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
9-
import org.togetherjava.tjbot.features.basic.SuggestionsUpDownVoter;
10-
import org.togetherjava.tjbot.features.basic.VcActivityCommand;
7+
import org.togetherjava.tjbot.features.basic.*;
118
import org.togetherjava.tjbot.features.bookmarks.*;
129
import org.togetherjava.tjbot.features.code.CodeMessageAutoDetection;
1310
import org.togetherjava.tjbot.features.code.CodeMessageHandler;
@@ -104,6 +101,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
104101
features.add(codeMessageHandler);
105102
features.add(new CodeMessageAutoDetection(config, codeMessageHandler));
106103
features.add(new CodeMessageManualDetection(codeMessageHandler));
104+
features.add(new SlashCommandEducator());
107105

108106
// Event receivers
109107
features.add(new RejoinModerationRoleListener(actionsStore, config));

application/src/main/java/org/togetherjava/tjbot/features/MessageReceiverAdapter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ public abstract class MessageReceiverAdapter implements MessageReceiver {
1818

1919
private final Pattern channelNamePattern;
2020

21+
/**
22+
* Creates an instance of a message receiver, listening to messages of all channels.
23+
*/
24+
protected MessageReceiverAdapter() {
25+
this(Pattern.compile(".*"));
26+
}
27+
2128
/**
2229
* Creates an instance of a message receiver with the given pattern.
2330
*
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.togetherjava.tjbot.features.basic;
2+
3+
import net.dv8tion.jda.api.EmbedBuilder;
4+
import net.dv8tion.jda.api.entities.Message;
5+
import net.dv8tion.jda.api.entities.MessageEmbed;
6+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
7+
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
8+
import net.dv8tion.jda.api.utils.FileUpload;
9+
10+
import org.togetherjava.tjbot.features.MessageReceiverAdapter;
11+
import org.togetherjava.tjbot.features.help.HelpSystemHelper;
12+
13+
import java.io.InputStream;
14+
import java.util.function.Predicate;
15+
import java.util.regex.Pattern;
16+
17+
/**
18+
* Listens to messages that are likely supposed to be message commands, such as {@code !foo} and
19+
* then educates the user about using slash commands, such as {@code /foo} instead.
20+
*/
21+
public final class SlashCommandEducator extends MessageReceiverAdapter {
22+
private static final String SLASH_COMMAND_POPUP_ADVICE_PATH = "slashCommandPopupAdvice.png";
23+
private static final Predicate<String> IS_MESSAGE_COMMAND = Pattern.compile("""
24+
[.!?] #Start of message command
25+
[a-zA-Z]{2,15} #Name of message command, e.g. 'close'
26+
.* #Rest of the message
27+
""", Pattern.COMMENTS).asMatchPredicate();
28+
29+
@Override
30+
public void onMessageReceived(MessageReceivedEvent event) {
31+
if (event.getAuthor().isBot() || event.isWebhookMessage()) {
32+
return;
33+
}
34+
35+
String content = event.getMessage().getContentRaw();
36+
if (IS_MESSAGE_COMMAND.test(content)) {
37+
sendAdvice(event.getMessage());
38+
}
39+
}
40+
41+
private void sendAdvice(Message message) {
42+
String content =
43+
"""
44+
Looks like you attempted to use a command? Please note that we only use **slash-commands** on this server 🙂
45+
46+
Try starting your message with a forward-slash `/` and Discord should open a popup showing you all available commands.
47+
A command might then look like `/foo` 👍""";
48+
49+
createReply(message, content, SLASH_COMMAND_POPUP_ADVICE_PATH).queue();
50+
}
51+
52+
private static MessageCreateAction createReply(Message messageToReplyTo, String content,
53+
String imagePath) {
54+
boolean useImage = true;
55+
InputStream imageData = HelpSystemHelper.class.getResourceAsStream("/" + imagePath);
56+
if (imageData == null) {
57+
useImage = false;
58+
}
59+
60+
MessageEmbed embed = new EmbedBuilder().setDescription(content)
61+
.setImage(useImage ? "attachment://" + imagePath : null)
62+
.build();
63+
64+
MessageCreateAction action = messageToReplyTo.replyEmbeds(embed);
65+
if (useImage) {
66+
action = action.addFiles(FileUpload.fromData(imageData, imagePath));
67+
}
68+
69+
return action;
70+
}
71+
}

application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageAutoDetection.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ public final class CodeMessageAutoDetection extends MessageReceiverAdapter {
3434
* @param codeMessageHandler to register detected code messages at for further handling
3535
*/
3636
public CodeMessageAutoDetection(Config config, CodeMessageHandler codeMessageHandler) {
37-
super(Pattern.compile(".*"));
38-
3937
this.codeMessageHandler = codeMessageHandler;
4038

4139
isHelpForumName =

application/src/main/java/org/togetherjava/tjbot/features/code/CodeMessageHandler.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import java.awt.Color;
3030
import java.util.*;
3131
import java.util.function.Function;
32-
import java.util.regex.Pattern;
3332
import java.util.stream.Collectors;
3433

3534
/**
@@ -66,8 +65,6 @@ public final class CodeMessageHandler extends MessageReceiverAdapter implements
6665
* Creates a new instance.
6766
*/
6867
public CodeMessageHandler() {
69-
super(Pattern.compile(".*"));
70-
7168
componentIdInteractor = new ComponentIdInteractor(getInteractionType(), getName());
7269

7370
List<CodeAction> codeActions = List.of(new FormatCodeCommand());

application/src/main/java/org/togetherjava/tjbot/features/filesharing/FileSharingMessageListener.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ public class FileSharingMessageListener extends MessageReceiverAdapter implement
7373
* @see org.togetherjava.tjbot.features.Features
7474
*/
7575
public FileSharingMessageListener(Config config) {
76-
super(Pattern.compile(".*"));
77-
7876
gistApiKey = config.getGistApiKey();
7977
isHelpForumName =
8078
Pattern.compile(config.getHelpSystem().getHelpForumPattern()).asMatchPredicate();

application/src/main/java/org/togetherjava/tjbot/features/moderation/attachment/BlacklistedAttachmentListener.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@
1515
import org.togetherjava.tjbot.features.moderation.modmail.ModMailCommand;
1616
import org.togetherjava.tjbot.features.utils.MessageUtils;
1717

18-
import java.awt.*;
18+
import java.awt.Color;
1919
import java.util.List;
2020
import java.util.Locale;
2121
import java.util.function.UnaryOperator;
22-
import java.util.regex.Pattern;
2322

2423
/**
2524
* Reacts to blacklisted attachments being posted, upon which they are deleted.
@@ -35,7 +34,6 @@ public final class BlacklistedAttachmentListener extends MessageReceiverAdapter
3534
* @param modAuditLogWriter to inform the mods about the suspicious attachment
3635
*/
3736
public BlacklistedAttachmentListener(Config config, ModAuditLogWriter modAuditLogWriter) {
38-
super(Pattern.compile(".*"));
3937
this.modAuditLogWriter = modAuditLogWriter;
4038
blacklistedFileExtensions = config.getBlacklistedFileExtensions();
4139
}

application/src/main/java/org/togetherjava/tjbot/features/moderation/scam/ScamBlocker.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ public final class ScamBlocker extends MessageReceiverAdapter implements UserInt
7474
*/
7575
public ScamBlocker(ModerationActionsStore actionsStore, ScamHistoryStore scamHistoryStore,
7676
Config config) {
77-
super(Pattern.compile(".*"));
78-
7977
this.actionsStore = actionsStore;
8078
this.scamHistoryStore = scamHistoryStore;
8179
this.config = config;

application/src/main/java/org/togetherjava/tjbot/features/tophelper/TopHelpersMessageListener.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@ public final class TopHelpersMessageListener extends MessageReceiverAdapter {
3838
* @param config the config to use for this
3939
*/
4040
public TopHelpersMessageListener(Database database, Config config) {
41-
super(Pattern.compile(".*"));
42-
4341
this.database = database;
4442

4543
isHelpForumName =
Loading
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.togetherjava.tjbot.features.basic;
2+
3+
import net.dv8tion.jda.api.entities.MessageEmbed;
4+
import net.dv8tion.jda.api.entities.channel.ChannelType;
5+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
6+
import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
7+
import net.dv8tion.jda.api.utils.messages.MessageCreateData;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.MethodSource;
11+
12+
import org.togetherjava.tjbot.features.MessageReceiver;
13+
import org.togetherjava.tjbot.jda.JdaTester;
14+
15+
import java.util.List;
16+
import java.util.stream.Stream;
17+
18+
import static org.mockito.ArgumentMatchers.any;
19+
import static org.mockito.Mockito.*;
20+
21+
final class SlashCommandEducatorTest {
22+
private JdaTester jdaTester;
23+
private MessageReceiver messageReceiver;
24+
25+
@BeforeEach
26+
void setUp() {
27+
jdaTester = new JdaTester();
28+
messageReceiver = new SlashCommandEducator();
29+
}
30+
31+
private MessageReceivedEvent sendMessage(String content) {
32+
MessageCreateData message = new MessageCreateBuilder().setContent(content).build();
33+
MessageReceivedEvent event =
34+
jdaTester.createMessageReceiveEvent(message, List.of(), ChannelType.TEXT);
35+
36+
messageReceiver.onMessageReceived(event);
37+
38+
return event;
39+
}
40+
41+
@ParameterizedTest
42+
@MethodSource("provideMessageCommands")
43+
void sendsAdviceOnMessageCommand(String message) {
44+
// GIVEN a message containing a message command
45+
// WHEN the message is sent
46+
MessageReceivedEvent event = sendMessage(message);
47+
48+
// THEN the system replies to it with an advice
49+
verify(event.getMessage(), times(1)).replyEmbeds(any(MessageEmbed.class));
50+
}
51+
52+
@ParameterizedTest
53+
@MethodSource("provideOtherMessages")
54+
void ignoresOtherMessages(String message) {
55+
// GIVEN a message that is not a message command
56+
// WHEN the message is sent
57+
MessageReceivedEvent event = sendMessage(message);
58+
59+
// THEN the system ignores the message and does not reply to it
60+
verify(event.getMessage(), never()).replyEmbeds(any(MessageEmbed.class));
61+
}
62+
63+
private static Stream<String> provideMessageCommands() {
64+
return Stream.of("!foo", ".foo", "?foo", ".test", "!whatever", "!this is a test");
65+
}
66+
67+
private static Stream<String> provideOtherMessages() {
68+
return Stream.of(" a ", "foo", "#foo", "/foo", "!!!", "?!?!?", "?", ".,-", "!f", "! foo");
69+
}
70+
}

application/src/test/java/org/togetherjava/tjbot/features/reminder/RemindRoutineTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,6 @@ void reminderIsNotSendIfNotPending() {
184184
private static void assertSimilar(Instant expected, Instant actual) {
185185
// NOTE For some reason, the instant ends up in the database slightly wrong already (about
186186
// half a second), seems to be an issue with jOOQ
187-
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(1));
187+
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(2));
188188
}
189189
}

application/src/test/java/org/togetherjava/tjbot/jda/JdaTester.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ public JdaTester() {
216216
doNothing().when(messageCreateAction).queue();
217217
when(messageCreateAction.setContent(any())).thenReturn(messageCreateAction);
218218
when(messageCreateAction.addContent(any())).thenReturn(messageCreateAction);
219+
when(messageCreateAction.addFiles(any(FileUpload.class))).thenReturn(messageCreateAction);
220+
when(messageCreateAction.addFiles(anyCollection())).thenReturn(messageCreateAction);
219221

220222
CacheRestAction<PrivateChannel> privateChannelAction =
221223
createSucceededActionMock(privateChannel, CacheRestAction.class);

0 commit comments

Comments
 (0)