Skip to content

Commit cc380d3

Browse files
authored
Merge pull request #425 from Java-Discord/unformatted-code-replacement
AutoCodeFormatter
2 parents 7e732dd + 02e50ff commit cc380d3

File tree

6 files changed

+404
-136
lines changed

6 files changed

+404
-136
lines changed

src/main/java/net/javadiscord/javabot/data/config/guild/HelpConfig.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,14 @@ public class HelpConfig extends GuildConfigItem {
4949
You can disable notifications like this using the `/preferences` command.
5050
[Post link](%s)
5151
""";
52-
53-
52+
53+
/**
54+
* The message that is sent in a post to tell users that they
55+
* should use discord's code-formatting, provided the bot detects unformatted code.
56+
* Issued by {@link net.javadiscord.javabot.systems.help.AutoCodeFormatter}
57+
*/
58+
private String formatHintMessage = "> Please format your code to make it more readable. \n> For java, it should look like this: \n```\u200B`\u200B`\u200B`\u200Bjava\npublic void foo() {\n \n}\n\u200B`\u200B`\u200B`\u200B```";
59+
5460
/**
5561
* The message that's sent when a user unreserved a channel where other users
5662
* participated in.
@@ -93,6 +99,12 @@ public class HelpConfig extends GuildConfigItem {
9399
*/
94100
private int minimumMessageLength = 10;
95101

102+
/**
103+
* The message-embed's footnote of an unformatted-code-replacement.
104+
* Issued by {@link net.javadiscord.javabot.systems.help.AutoCodeFormatter}
105+
*/
106+
private String autoFormatInfoMessage = "This message has been formatted automatically. You can disable this using ``/preferences``.";
107+
96108
/**
97109
* The amount of experience points one gets for being thanked by the help channel owner.
98110
*/

src/main/java/net/javadiscord/javabot/listener/HugListener.java

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import lombok.RequiredArgsConstructor;
44
import lombok.extern.slf4j.Slf4j;
5-
import net.dv8tion.jda.api.entities.Message;
6-
import net.dv8tion.jda.api.entities.Webhook;
75
import net.dv8tion.jda.api.entities.channel.ChannelType;
86
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
97
import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildChannel;
@@ -24,16 +22,59 @@
2422
@Slf4j
2523
@RequiredArgsConstructor
2624
public class HugListener extends ListenerAdapter {
27-
private static final Pattern FUCKER = Pattern.compile("(fuck)(ing|er|ed|k+)?", Pattern.CASE_INSENSITIVE);
25+
private static final Pattern FUCKER = Pattern.compile(
26+
"(fuck)(ing|er|ed|k+)?",
27+
Pattern.CASE_INSENSITIVE
28+
);
2829
private final AutoMod autoMod;
2930
private final BotConfig botConfig;
3031

32+
private static String processHug(String originalText) {
33+
// FucK -> HuG, FuCk -> Hug
34+
return String.valueOf(copyCase(originalText, 0, 'h')) + copyCase(originalText, 1, 'u') +
35+
copyCase(originalText, 3, 'g');
36+
}
37+
38+
private static String replaceFucks(String str) {
39+
return FUCKER.matcher(str).replaceAll(matchResult -> {
40+
String theFuck = matchResult.group(1);
41+
String suffix = Objects.requireNonNullElse(matchResult.group(2), "");
42+
String processedSuffix = switch (suffix.toLowerCase()) {
43+
case "er", "ed", "ing" ->
44+
copyCase(suffix, 0, 'g') + suffix; // fucking, fucker, fucked
45+
case "" -> ""; // just fuck
46+
default -> copyCase(suffix, "g".repeat(suffix.length())); // fuckkkkk...
47+
};
48+
return processHug(theFuck) + processedSuffix;
49+
});
50+
}
51+
52+
private static String copyCase(String source, String toChange) {
53+
if (source.length() != toChange.length()) {
54+
throw new IllegalArgumentException("lengths differ");
55+
}
56+
StringBuilder sb = new StringBuilder();
57+
for (int i = 0; i < source.length(); i++) {
58+
sb.append(copyCase(source, i, toChange.charAt(i)));
59+
}
60+
return sb.toString();
61+
}
62+
63+
private static char copyCase(String original, int index, char newChar) {
64+
if (Character.isUpperCase(original.charAt(index))) {
65+
return Character.toUpperCase(newChar);
66+
} else {
67+
return newChar;
68+
}
69+
}
70+
3171
@Override
3272
public void onMessageReceived(@Nonnull MessageReceivedEvent event) {
3373
if (!event.isFromGuild()) {
3474
return;
3575
}
36-
if (autoMod.hasSuspiciousLink(event.getMessage()) || autoMod.hasAdvertisingLink(event.getMessage())) {
76+
if (autoMod.hasSuspiciousLink(event.getMessage()) ||
77+
autoMod.hasAdvertisingLink(event.getMessage())) {
3778
return;
3879
}
3980
if (!event.getMessage().getMentions().getUsers().isEmpty()) {
@@ -42,7 +83,8 @@ public void onMessageReceived(@Nonnull MessageReceivedEvent event) {
4283
if (event.isWebhookMessage()) {
4384
return;
4485
}
45-
if (event.getChannel().getIdLong() == botConfig.get(event.getGuild()).getModerationConfig()
86+
if (event.getChannel().getIdLong() == botConfig.get(event.getGuild())
87+
.getModerationConfig()
4688
.getSuggestionChannelId()) {
4789
return;
4890
}
@@ -51,65 +93,30 @@ public void onMessageReceived(@Nonnull MessageReceivedEvent event) {
5193
tc = event.getChannel().asTextChannel();
5294
}
5395
if (event.isFromThread()) {
54-
StandardGuildChannel parentChannel = event.getChannel().asThreadChannel().getParentChannel().asStandardGuildChannel();
96+
StandardGuildChannel parentChannel = event.getChannel()
97+
.asThreadChannel()
98+
.getParentChannel()
99+
.asStandardGuildChannel();
55100
if (parentChannel instanceof TextChannel textChannel) {
56101
tc = textChannel;
57102
}
58103
}
59104
if (tc == null) {
60105
return;
106+
61107
}
62108
String content = event.getMessage().getContentRaw();
63109
if (FUCKER.matcher(content).find()) {
64110
long threadId = event.isFromThread() ? event.getChannel().getIdLong() : 0;
65-
WebhookUtil.ensureWebhookExists(tc,
66-
wh -> sendWebhookMessage(wh, event.getMessage(), replaceFucks(content), threadId),
67-
e -> ExceptionLogger.capture(e, getClass().getSimpleName()));
68-
}
69-
}
70-
71-
private static String processHug(String originalText) {
72-
// FucK -> HuG, FuCk -> Hug
73-
return String.valueOf(copyCase(originalText, 0, 'h'))
74-
+ copyCase(originalText, 1, 'u')
75-
+ copyCase(originalText, 3, 'g');
76-
}
77-
78-
private static String replaceFucks(String str) {
79-
return FUCKER.matcher(str).replaceAll(matchResult -> {
80-
String theFuck = matchResult.group(1);
81-
String suffix = Objects.requireNonNullElse(matchResult.group(2), "");
82-
String processedSuffix = switch(suffix.toLowerCase()) {
83-
case "er", "ed", "ing" -> copyCase(suffix, 0, 'g') + suffix; // fucking, fucker, fucked
84-
case "" -> ""; // just fuck
85-
default -> copyCase(suffix, "g".repeat(suffix.length())); // fuckkkkk...
86-
};
87-
return processHug(theFuck) + processedSuffix;
88-
});
89-
}
90-
91-
private static String copyCase(String source, String toChange) {
92-
if (source.length() != toChange.length()) throw new IllegalArgumentException("lengths differ");
93-
StringBuilder sb = new StringBuilder();
94-
for (int i = 0; i < source.length(); i++) {
95-
sb.append(copyCase(source, i, toChange.charAt(i)));
111+
WebhookUtil.ensureWebhookExists(
112+
tc,
113+
wh -> WebhookUtil.replaceMemberMessage(wh, event.getMessage(),
114+
replaceFucks(content), threadId
115+
),
116+
e -> ExceptionLogger.capture(e, getClass().getSimpleName())
117+
);
96118
}
97-
return sb.toString();
98119
}
99120

100-
private static char copyCase(String original, int index, char newChar) {
101-
if (Character.isUpperCase(original.charAt(index))) {
102-
return Character.toUpperCase(newChar);
103-
} else {
104-
return newChar;
105-
}
106-
}
107121

108-
private void sendWebhookMessage(Webhook webhook, Message originalMessage, String newMessageContent, long threadId) {
109-
WebhookUtil.mirrorMessageToWebhook(webhook, originalMessage, newMessageContent, threadId, null, null)
110-
.thenAccept(unused -> originalMessage.delete().queue()).exceptionally(e -> {
111-
ExceptionLogger.capture(e, getClass().getSimpleName());
112-
return null;
113-
});
114-
}
115122
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package net.javadiscord.javabot.systems.help;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import net.dv8tion.jda.api.EmbedBuilder;
5+
import net.dv8tion.jda.api.entities.Guild;
6+
import net.dv8tion.jda.api.entities.MessageEmbed;
7+
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
8+
import net.dv8tion.jda.api.interactions.components.buttons.Button;
9+
import net.javadiscord.javabot.data.config.BotConfig;
10+
import net.javadiscord.javabot.systems.moderation.AutoMod;
11+
import net.javadiscord.javabot.systems.user_preferences.UserPreferenceService;
12+
import net.javadiscord.javabot.systems.user_preferences.model.Preference;
13+
import net.javadiscord.javabot.util.ExceptionLogger;
14+
import net.javadiscord.javabot.util.InteractionUtils;
15+
import net.javadiscord.javabot.util.WebhookUtil;
16+
import org.jetbrains.annotations.NotNull;
17+
import org.jetbrains.annotations.Nullable;
18+
import org.springframework.stereotype.Component;
19+
20+
import javax.annotation.Nonnull;
21+
import java.util.Objects;
22+
23+
/**
24+
* Handles auto-formatting of code in help-channels.
25+
* A non-listener class since it has to be called strictly after {@link HelpListener} to not send its messages before channel reserving.
26+
*/
27+
@RequiredArgsConstructor
28+
@Component
29+
public class AutoCodeFormatter {
30+
private final AutoMod autoMod;
31+
private final BotConfig botConfig;
32+
private final UserPreferenceService preferenceService;
33+
34+
35+
/**
36+
* Method responsible for finding a place to insert a codeblock, if present.
37+
*
38+
* @param event a {@link MessageReceivedEvent}.
39+
* @return a MessageCodeblock instance, holding a startIndex, content and
40+
* an endIndex. Returns null if no place was found.
41+
*/
42+
@Nullable
43+
private static CodeBlock findCodeblock(@NotNull MessageReceivedEvent event) {
44+
String msg = event.getMessage().getContentRaw();
45+
int openingBracket = msg.indexOf("{");
46+
int closingBracket = msg.lastIndexOf("}");
47+
if (closingBracket == -1 || openingBracket == -1) {
48+
return null;
49+
}
50+
int startIndex = msg.lastIndexOf("\n", openingBracket);
51+
int endIndex = msg.indexOf("\n", closingBracket);
52+
if (startIndex == -1) {
53+
startIndex = 0;
54+
}
55+
if (endIndex == -1) {
56+
endIndex = msg.length();
57+
}
58+
return new CodeBlock(startIndex, endIndex);
59+
}
60+
61+
/**
62+
* called by {@link HelpListener#onMessageReceived(MessageReceivedEvent)} on every message.
63+
* It is worth noting that this class can't register as an event handler itself due to there being no way of
64+
* setting its methods to be strictly called after {@link HelpListener}.
65+
*
66+
* @param event a {@link MessageReceivedEvent}
67+
* @param isFirstMessage flag that should be set if the message is a thread-opening one.
68+
*/
69+
void handleMessageEvent(@Nonnull MessageReceivedEvent event, boolean isFirstMessage) {
70+
if (!event.isFromGuild()) {
71+
return;
72+
}
73+
if (event.getAuthor().isBot() || event.getMessage()
74+
.getAuthor()
75+
.isSystem()) {
76+
return;
77+
}
78+
if (autoMod.hasSuspiciousLink(event.getMessage()) ||
79+
autoMod.hasAdvertisingLink(event.getMessage())) {
80+
return;
81+
}
82+
if (event.isWebhookMessage()) {
83+
return;
84+
}
85+
if (!event.isFromThread()) {
86+
return;
87+
}
88+
if (event.getChannel()
89+
.asThreadChannel()
90+
.getParentChannel()
91+
.getIdLong() != botConfig.get(event.getGuild())
92+
.getHelpConfig()
93+
.getHelpForumChannelId()) {
94+
return;
95+
}
96+
if (!Boolean.parseBoolean(preferenceService.getOrCreate(Objects.requireNonNull(event.getMember())
97+
.getIdLong(), Preference.FORMAT_UNFORMATTED_CODE).getState())) {
98+
return;
99+
}
100+
101+
102+
if (event.getMessage().getContentRaw().contains("```")) {
103+
return; // exit if already contains codeblock
104+
}
105+
106+
CodeBlock code = findCodeblock(event);
107+
if (code == null) {
108+
return;
109+
}
110+
111+
if (isFirstMessage || !event.getMessage().getMentions().getUsers().isEmpty() ||
112+
!event.getMessage().getMentions().getRoles().isEmpty() ||
113+
event.getMessage().getMentions().mentionsEveryone()) {
114+
sendFormatHint(event);
115+
} else {
116+
replaceUnformattedCode(event.getMessage()
117+
.getContentRaw(), code.startIndex(), code.endIndex(), event);
118+
}
119+
}
120+
121+
private void sendFormatHint(MessageReceivedEvent event) {
122+
event.getMessage()
123+
.replyEmbeds(formatHintEmbed(event.getGuild()))
124+
.addActionRow(
125+
Button.secondary(InteractionUtils.DELETE_ORIGINAL_TEMPLATE, "\uD83D\uDDD1️")
126+
)
127+
.queue();
128+
}
129+
130+
private void replaceUnformattedCode(String msg, int codeStartIndex, int codeEndIndex, MessageReceivedEvent event) {
131+
// default case: a "normal", non-ping containing, non first message of a forum-thread containing "{" and "}".
132+
// user must also have set their preferences to allow this.
133+
if (msg.length() > 1992) { // can't exceed discord's char limit
134+
sendFormatHint(event);
135+
return;
136+
}
137+
String messageContent = msg.substring(0, codeStartIndex) + " ```" +
138+
msg.substring(codeStartIndex, codeEndIndex) + " ```" + msg.substring(codeEndIndex);
139+
EmbedBuilder autoformatInfo = new EmbedBuilder().setDescription(botConfig.get(event.getGuild())
140+
.getHelpConfig()
141+
.getAutoFormatInfoMessage());
142+
WebhookUtil.ensureWebhookExists(
143+
event.getChannel()
144+
.asThreadChannel()
145+
.getParentChannel()
146+
.asForumChannel(),
147+
wh -> WebhookUtil.replaceMemberMessage(
148+
wh,
149+
event.getMessage(),
150+
messageContent,
151+
event.getChannel().getIdLong(),
152+
autoformatInfo.build()
153+
),
154+
e -> ExceptionLogger.capture(
155+
e,
156+
"Error creating webhook for UnformattedCodeListener"
157+
)
158+
);
159+
}
160+
161+
private MessageEmbed formatHintEmbed(Guild guild) {
162+
return new EmbedBuilder().setDescription(botConfig.get(guild)
163+
.getHelpConfig()
164+
.getFormatHintMessage()).build();
165+
}
166+
167+
private record CodeBlock(int startIndex, int endIndex) {}
168+
}

0 commit comments

Comments
 (0)