Skip to content

Commit 2679a2f

Browse files
committed
Resolve merge conflicts
Signed-off-by: Chris Sdogkos <work@chris-sdogkos.com>
2 parents e93861d + e43f7c3 commit 2679a2f

18 files changed

+758
-84
lines changed

application/build.gradle

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ dependencies {
5757

5858
implementation 'io.mikael:urlbuilder:2.0.9'
5959

60-
implementation 'org.jsoup:jsoup:1.21.1'
60+
implementation 'org.jsoup:jsoup:1.22.1'
6161

6262
implementation 'org.scilab.forge:jlatexmath:1.0.7'
6363
implementation 'org.scilab.forge:jlatexmath-font-greek:1.0.7'
@@ -69,7 +69,7 @@ dependencies {
6969
implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion"
7070
implementation "com.sigpwned:jackson-modules-java17-sealed-classes:2.19.0.0"
7171

72-
implementation 'com.github.freva:ascii-table:1.8.0'
72+
implementation 'com.github.freva:ascii-table:1.9.0'
7373

7474
implementation 'io.github.url-detector:url-detector:0.1.23'
7575

@@ -86,8 +86,7 @@ dependencies {
8686
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
8787
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
8888

89-
implementation "com.theokanning.openai-gpt3-java:api:$chatGPTVersion"
90-
implementation "com.theokanning.openai-gpt3-java:service:$chatGPTVersion"
89+
implementation "com.openai:openai-java:$chatGPTVersion"
9190
}
9291

9392
application {

application/config.json.template

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@
194194
"videoLinkPattern": "http(s)?://www\\.youtube.com.*",
195195
"pollIntervalInMinutes": 10
196196
},
197+
"quoteBoardConfig": {
198+
"minimumReactionsToTrigger": 5,
199+
"channel": "quotes",
200+
"reactionEmoji": "⭐"
201+
},
197202
"memberCountCategoryPattern": "Info",
198203
"topHelpers": {
199204
"rolePattern": "Top Helper.*",

application/src/main/java/org/togetherjava/tjbot/config/Config.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public final class Config {
4848
private final RSSFeedsConfig rssFeedsConfig;
4949
private final String selectRolesChannelPattern;
5050
private final String memberCountCategoryPattern;
51+
private final QuoteBoardConfig quoteBoardConfig;
5152
private final TopHelpersConfig topHelpers;
5253
private final List<String> dynamicVoiceChannelPatterns;
5354

@@ -103,6 +104,8 @@ private Config(@JsonProperty(value = "token", required = true) String token,
103104
@JsonProperty(value = "rssConfig", required = true) RSSFeedsConfig rssFeedsConfig,
104105
@JsonProperty(value = "selectRolesChannelPattern",
105106
required = true) String selectRolesChannelPattern,
107+
@JsonProperty(value = "quoteBoardConfig",
108+
required = true) QuoteBoardConfig quoteBoardConfig,
106109
@JsonProperty(value = "topHelpers", required = true) TopHelpersConfig topHelpers,
107110
@JsonProperty(value = "dynamicVoiceChannelPatterns",
108111
required = true) List<String> dynamicVoiceChannelPatterns) {
@@ -140,6 +143,7 @@ private Config(@JsonProperty(value = "token", required = true) String token,
140143
this.featureBlacklistConfig = Objects.requireNonNull(featureBlacklistConfig);
141144
this.rssFeedsConfig = Objects.requireNonNull(rssFeedsConfig);
142145
this.selectRolesChannelPattern = Objects.requireNonNull(selectRolesChannelPattern);
146+
this.quoteBoardConfig = Objects.requireNonNull(quoteBoardConfig);
143147
this.topHelpers = Objects.requireNonNull(topHelpers);
144148
this.dynamicVoiceChannelPatterns = Objects.requireNonNull(dynamicVoiceChannelPatterns);
145149
}
@@ -435,6 +439,18 @@ public String getSelectRolesChannelPattern() {
435439
return selectRolesChannelPattern;
436440
}
437441

442+
/**
443+
* The configuration of the quote messages config.
444+
*
445+
* <p>
446+
* >The configuration of the quote board feature. Quotes user selected messages.
447+
*
448+
* @return configuration of quote messages config
449+
*/
450+
public QuoteBoardConfig getQuoteBoardConfig() {
451+
return quoteBoardConfig;
452+
}
453+
438454
/**
439455
* Gets the pattern matching the category that is used to display the total member count.
440456
*
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.togetherjava.tjbot.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.annotation.JsonRootName;
5+
import org.apache.logging.log4j.LogManager;
6+
7+
import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;
8+
9+
import java.util.Objects;
10+
11+
/**
12+
* Configuration for the quote board feature, see {@link QuoteBoardForwarder}.
13+
*/
14+
@JsonRootName("quoteBoardConfig")
15+
public record QuoteBoardConfig(
16+
@JsonProperty(value = "minimumReactionsToTrigger", required = true) int minimumReactions,
17+
@JsonProperty(required = true) String channel,
18+
@JsonProperty(value = "reactionEmoji", required = true) String reactionEmoji) {
19+
20+
/**
21+
* Creates a QuoteBoardConfig.
22+
*
23+
* @param minimumReactions the minimum amount of reactions
24+
* @param channel the pattern for the board channel
25+
* @param reactionEmoji the emoji with which users should react to
26+
*/
27+
public QuoteBoardConfig {
28+
if (minimumReactions <= 0) {
29+
throw new IllegalArgumentException("minimumReactions must be greater than zero");
30+
}
31+
Objects.requireNonNull(channel);
32+
if (channel.isBlank()) {
33+
throw new IllegalArgumentException("channel must not be empty or blank");
34+
}
35+
Objects.requireNonNull(reactionEmoji);
36+
if (reactionEmoji.isBlank()) {
37+
throw new IllegalArgumentException("reactionEmoji must not be empty or blank");
38+
}
39+
LogManager.getLogger(QuoteBoardConfig.class)
40+
.debug("Quote-Board configs loaded: minimumReactions={}, channel='{}', reactionEmoji='{}'",
41+
minimumReactions, channel, reactionEmoji);
42+
}
43+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.togetherjava.tjbot.db.Database;
99
import org.togetherjava.tjbot.features.basic.MemberCountDisplayRoutine;
1010
import org.togetherjava.tjbot.features.basic.PingCommand;
11+
import org.togetherjava.tjbot.features.basic.QuoteBoardForwarder;
1112
import org.togetherjava.tjbot.features.basic.RoleSelectCommand;
1213
import org.togetherjava.tjbot.features.basic.SlashCommandEducator;
1314
import org.togetherjava.tjbot.features.basic.SuggestionsUpDownVoter;
@@ -39,6 +40,7 @@
3940
import org.togetherjava.tjbot.features.mathcommands.TeXCommand;
4041
import org.togetherjava.tjbot.features.mathcommands.wolframalpha.WolframAlphaCommand;
4142
import org.togetherjava.tjbot.features.mediaonly.MediaOnlyChannelListener;
43+
import org.togetherjava.tjbot.features.messages.MessageCommand;
4244
import org.togetherjava.tjbot.features.moderation.BanCommand;
4345
import org.togetherjava.tjbot.features.moderation.KickCommand;
4446
import org.togetherjava.tjbot.features.moderation.ModerationActionsStore;
@@ -161,6 +163,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
161163
features.add(new CodeMessageManualDetection(codeMessageHandler));
162164
features.add(new SlashCommandEducator());
163165
features.add(new PinnedNotificationRemover(config));
166+
features.add(new QuoteBoardForwarder(config));
164167

165168
// Voice receivers
166169
features.add(new DynamicVoiceChat(config));
@@ -207,6 +210,7 @@ public static Collection<Feature> createFeatures(JDA jda, Database database, Con
207210
features.add(new BookmarksCommand(bookmarksSystem));
208211
features.add(new ChatGptCommand(chatGptService, helpSystemHelper));
209212
features.add(new JShellCommand(jshellEval));
213+
features.add(new MessageCommand());
210214

211215
FeatureBlacklist<Class<?>> blacklist = blacklistConfig.normal();
212216
return blacklist.filterStream(features.stream(), Object::getClass).toList();

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
44
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
55
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
6+
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
67

78
import java.util.regex.Pattern;
89

@@ -56,4 +57,13 @@ public interface MessageReceiver extends Feature {
5657
* message that was deleted
5758
*/
5859
void onMessageDeleted(MessageDeleteEvent event);
60+
61+
/**
62+
* Triggered by the core system whenever a new reaction was added to a message in a text channel
63+
* of a guild the bot has been added to.
64+
*
65+
* @param event the event that triggered this, containing information about the corresponding
66+
* reaction that was added
67+
*/
68+
void onMessageReactionAdd(MessageReactionAddEvent event);
5969
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import net.dv8tion.jda.api.events.message.MessageDeleteEvent;
44
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
55
import net.dv8tion.jda.api.events.message.MessageUpdateEvent;
6+
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
67

78
import java.util.regex.Pattern;
89

@@ -57,4 +58,10 @@ public void onMessageUpdated(MessageUpdateEvent event) {
5758
public void onMessageDeleted(MessageDeleteEvent event) {
5859
// Adapter does not react by default, subclasses may change this behavior
5960
}
61+
62+
@SuppressWarnings("NoopMethodInAbstractClass")
63+
@Override
64+
public void onMessageReactionAdd(MessageReactionAddEvent event) {
65+
// Adapter does not react by default, subclasses may change this behavior
66+
}
6067
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88

99
import java.util.regex.Pattern;
1010

11+
/**
12+
* Adapter implementation of a {@link VoiceReceiver}. A new receiver can then be registered by
13+
* adding it to {@link Features}.
14+
* <p>
15+
* {@link #onVoiceUpdate(GuildVoiceUpdateEvent)} like the other provided methods can be overridden
16+
* if desired. The default implementation is empty, the adapter will not react to such events.
17+
*/
1118
public class VoiceReceiverAdapter implements VoiceReceiver {
1219

1320
private final Pattern channelNamePattern;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package org.togetherjava.tjbot.features.basic;
2+
3+
import net.dv8tion.jda.api.JDA;
4+
import net.dv8tion.jda.api.entities.Guild;
5+
import net.dv8tion.jda.api.entities.Message;
6+
import net.dv8tion.jda.api.entities.MessageReaction;
7+
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
8+
import net.dv8tion.jda.api.entities.emoji.Emoji;
9+
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
10+
import net.dv8tion.jda.api.requests.RestAction;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
14+
import org.togetherjava.tjbot.config.Config;
15+
import org.togetherjava.tjbot.config.QuoteBoardConfig;
16+
import org.togetherjava.tjbot.features.MessageReceiverAdapter;
17+
18+
import java.util.List;
19+
import java.util.Optional;
20+
import java.util.function.Predicate;
21+
import java.util.regex.Pattern;
22+
23+
/**
24+
* Listens for reaction-add events and turns popular messages into "quotes".
25+
* <p>
26+
* When someone reacts to a message with the configured emoji, the listener counts how many users
27+
* have used that same emoji. If the total meets or exceeds the minimum threshold and the bot has
28+
* not processed the message before, it copies (forwards) the message to the first text channel
29+
* whose name matches the configured quote-board pattern, then reacts to the original message itself
30+
* to mark it as handled (and to not let people spam react a message and give a way to the bot to
31+
* know that a message has been quoted before).
32+
* <p>
33+
* Key points: - Trigger emoji, minimum vote count and quote-board channel pattern are supplied via
34+
* {@code QuoteBoardConfig}.
35+
*/
36+
public final class QuoteBoardForwarder extends MessageReceiverAdapter {
37+
38+
private static final Logger logger = LoggerFactory.getLogger(QuoteBoardForwarder.class);
39+
private final Emoji triggerReaction;
40+
private final Predicate<String> isQuoteBoardChannelName;
41+
private final QuoteBoardConfig config;
42+
43+
/**
44+
* Constructs a new instance of QuoteBoardForwarder.
45+
*
46+
* @param config the configuration containing settings specific to the cool messages board,
47+
* including the reaction emoji and the pattern to match board channel names
48+
*/
49+
public QuoteBoardForwarder(Config config) {
50+
this.config = config.getQuoteBoardConfig();
51+
this.triggerReaction = Emoji.fromUnicode(this.config.reactionEmoji());
52+
53+
this.isQuoteBoardChannelName = Pattern.compile(this.config.channel()).asMatchPredicate();
54+
}
55+
56+
@Override
57+
public void onMessageReactionAdd(MessageReactionAddEvent event) {
58+
logger.debug("Received MessageReactionAddEvent: messageId={}, channelId={}, userId={}",
59+
event.getMessageId(), event.getChannel().getId(), event.getUserId());
60+
61+
final MessageReaction messageReaction = event.getReaction();
62+
63+
if (!messageReaction.getEmoji().equals(triggerReaction)) {
64+
logger.debug("Reaction emoji '{}' does not match trigger emoji '{}'. Ignoring.",
65+
messageReaction.getEmoji(), triggerReaction);
66+
return;
67+
}
68+
69+
if (hasAlreadyForwardedMessage(event.getJDA(), messageReaction)) {
70+
logger.debug("Message has already been forwarded by the bot. Skipping.");
71+
return;
72+
}
73+
74+
long reactionCount = messageReaction.retrieveUsers().stream().count();
75+
if (reactionCount < config.minimumReactions()) {
76+
logger.debug("Reaction count {} is less than minimum required {}. Skipping.",
77+
reactionCount, config.minimumReactions());
78+
return;
79+
}
80+
81+
final long guildId = event.getGuild().getIdLong();
82+
83+
Optional<TextChannel> boardChannel = findQuoteBoardChannel(event.getJDA(), guildId);
84+
85+
if (boardChannel.isEmpty()) {
86+
logger.warn(
87+
"Could not find board channel with pattern '{}' in server with ID '{}'. Skipping reaction handling...",
88+
this.config.channel(), guildId);
89+
return;
90+
}
91+
92+
logger.debug("Forwarding message to quote board channel: {}", boardChannel.get().getName());
93+
94+
event.retrieveMessage()
95+
.queue(message -> markAsProcessed(message)
96+
.flatMap(v -> message.forwardTo(boardChannel.orElseThrow()))
97+
.queue(_ -> logger.debug("Message forwarded to quote board channel: {}",
98+
boardChannel.get().getName())),
99+
100+
e -> logger.warn(
101+
"Unknown error while attempting to retrieve and forward message for quote-board, message is ignored.",
102+
e));
103+
104+
}
105+
106+
private RestAction<Void> markAsProcessed(Message message) {
107+
return message.addReaction(triggerReaction);
108+
}
109+
110+
/**
111+
* Gets the board text channel where the quotes go to, wrapped in an optional.
112+
*
113+
* @param jda the JDA
114+
* @param guildId the guild ID
115+
* @return the board text channel
116+
*/
117+
private Optional<TextChannel> findQuoteBoardChannel(JDA jda, long guildId) {
118+
Guild guild = jda.getGuildById(guildId);
119+
120+
if (guild == null) {
121+
throw new IllegalStateException(
122+
String.format("Guild with ID '%d' not found.", guildId));
123+
}
124+
125+
List<TextChannel> matchingChannels = guild.getTextChannelCache()
126+
.stream()
127+
.filter(channel -> isQuoteBoardChannelName.test(channel.getName()))
128+
.toList();
129+
130+
if (matchingChannels.size() > 1) {
131+
logger.warn(
132+
"Multiple quote board channels found matching pattern '{}' in guild with ID '{}'. Selecting the first one anyway.",
133+
this.config.channel(), guildId);
134+
}
135+
136+
return matchingChannels.stream().findFirst();
137+
}
138+
139+
/**
140+
* Checks a {@link MessageReaction} to see if the bot has reacted to it.
141+
*/
142+
private boolean hasAlreadyForwardedMessage(JDA jda, MessageReaction messageReaction) {
143+
if (!triggerReaction.equals(messageReaction.getEmoji())) {
144+
return false;
145+
}
146+
147+
return messageReaction.retrieveUsers()
148+
.parallelStream()
149+
.anyMatch(user -> jda.getSelfUser().getIdLong() == user.getIdLong());
150+
}
151+
}

application/src/main/java/org/togetherjava/tjbot/features/chatgpt/ChatGptCommand.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* which it will respond with an AI generated answer.
2626
*/
2727
public final class ChatGptCommand extends SlashCommandAdapter {
28+
private static final ChatGptModel CHAT_GPT_MODEL = ChatGptModel.HIGH_QUALITY;
2829
public static final String COMMAND_NAME = "chatgpt";
2930
private static final String QUESTION_INPUT = "question";
3031
private static final int MAX_MESSAGE_INPUT_LENGTH = 200;
@@ -82,8 +83,8 @@ public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
8283

8384
String question = event.getValue(QUESTION_INPUT).getAsString();
8485

85-
Optional<String> chatgptResponse =
86-
chatGptService.ask(question, "You may use markdown syntax for the response");
86+
Optional<String> chatgptResponse = chatGptService.ask(question,
87+
"You may use markdown syntax for the response", CHAT_GPT_MODEL);
8788
if (chatgptResponse.isPresent()) {
8889
userIdToAskedAtCache.put(event.getMember().getId(), Instant.now());
8990
}
@@ -96,7 +97,8 @@ public void onModalSubmitted(ModalInteractionEvent event, List<String> args) {
9697
String response = chatgptResponse.orElse(errorResponse);
9798
SelfUser selfUser = event.getJDA().getSelfUser();
9899

99-
MessageEmbed responseEmbed = helper.generateGptResponseEmbed(response, selfUser, question);
100+
MessageEmbed responseEmbed =
101+
helper.generateGptResponseEmbed(response, selfUser, question, CHAT_GPT_MODEL);
100102

101103
event.getHook().sendMessageEmbeds(responseEmbed).queue();
102104
}

0 commit comments

Comments
 (0)